From f521f3ba4ce6bcf93737aee69e8cb554a5f11e14 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 29 Jan 2015 00:48:24 -0500 Subject: [PATCH 001/195] VRFS-2689 new ruby models supporting db schema updates --- ruby/lib/jam_ruby.rb | 2 ++ ruby/lib/jam_ruby/models/performance_sample.rb | 4 ++++ ruby/lib/jam_ruby/models/user_presence.rb | 4 ++++ web/app/views/api_users/index.rabl | 3 ++- web/app/views/api_users/show.rabl | 3 ++- 5 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 ruby/lib/jam_ruby/models/performance_sample.rb create mode 100644 ruby/lib/jam_ruby/models/user_presence.rb diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 084ae7d9e..ea91db0fe 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -199,6 +199,8 @@ require "jam_ruby/models/video_source" require "jam_ruby/models/recorded_video" require "jam_ruby/models/text_message" require "jam_ruby/jam_tracks_manager" +require "jam_ruby/models/performance_sample" +require "jam_ruby/models/user_presence" include Jampb diff --git a/ruby/lib/jam_ruby/models/performance_sample.rb b/ruby/lib/jam_ruby/models/performance_sample.rb new file mode 100644 index 000000000..474ad7a32 --- /dev/null +++ b/ruby/lib/jam_ruby/models/performance_sample.rb @@ -0,0 +1,4 @@ +module JamRuby + class PerformanceSample < ActiveRecord::Base + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/user_presence.rb b/ruby/lib/jam_ruby/models/user_presence.rb new file mode 100644 index 000000000..d0274ef9a --- /dev/null +++ b/ruby/lib/jam_ruby/models/user_presence.rb @@ -0,0 +1,4 @@ +module JamRuby + class UserPresence < ActiveRecord::Base + end +end \ No newline at end of file diff --git a/web/app/views/api_users/index.rabl b/web/app/views/api_users/index.rabl index 27eb79ce0..00ecf01a6 100644 --- a/web/app/views/api_users/index.rabl +++ b/web/app/views/api_users/index.rabl @@ -1,4 +1,5 @@ collection @users # do not retrieve all child collections when showing a list of users -attributes :id, :first_name, :last_name, :name, :city, :state, :country, :email, :online, :musician, :photo_url, :biography +attributes :id, :first_name, :last_name, :name, :city, :state, :country, :email, :online, :musician, :photo_url, :biography, :age, :website, :skill_level, :concert_count, :studio_session_count, :virtual_band, :virtual_band_commitment, :traditional_band, :traditional_band_commitment, :traditional_band_touring, :paid_sessions, :paid_sessions_hourly_rate, +:paid_sessions_daily_rate, :free_sessions, :cowriting, :cowriting_purpose diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl index b005b0fa8..d402ff6a7 100644 --- a/web/app/views/api_users/show.rabl +++ b/web/app/views/api_users/show.rabl @@ -1,6 +1,7 @@ object @user -attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count +attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count, :age, :website, :skill_level, :concert_count, :studio_session_count, :virtual_band, :virtual_band_commitment, :traditional_band, :traditional_band_commitment, :traditional_band_touring, :paid_sessions, :paid_sessions_hourly_rate, +:paid_sessions_daily_rate, :free_sessions, :cowriting, :cowriting_purpose if @user.musician? node :location do @user.location end From 8d19e4510ecabe69beea9f4aae1f2bd06d80173b Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 31 Jan 2015 16:07:34 -0500 Subject: [PATCH 002/195] VRFS-2693 VRFS-2694 wip new musician profile models --- ruby/lib/jam_ruby/models/performance_sample.rb | 15 +++++++++++++++ ruby/lib/jam_ruby/models/user.rb | 3 +++ ruby/lib/jam_ruby/models/user_presence.rb | 5 +++++ ruby/spec/jam_ruby/models/user_spec.rb | 5 ++++- web/app/controllers/api_users_controller.rb | 5 ++++- web/config/routes.rb | 3 +++ 6 files changed, 34 insertions(+), 2 deletions(-) diff --git a/ruby/lib/jam_ruby/models/performance_sample.rb b/ruby/lib/jam_ruby/models/performance_sample.rb index 474ad7a32..a406effbc 100644 --- a/ruby/lib/jam_ruby/models/performance_sample.rb +++ b/ruby/lib/jam_ruby/models/performance_sample.rb @@ -1,4 +1,19 @@ module JamRuby class PerformanceSample < ActiveRecord::Base + belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id" + belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording" :foreign_key => "claimed_recording_id" + + validates :user_type_recording_unique :if => lambda { |p| p.type == "jamkazam" } + validates :user_type_service_unique :if => lambda { |p| p.type != "jamkazam" } + + def user_type_recording_unique + match = PerformanceSample.exists?(:user_id => user.id, :claimed_recording_id => self.claimed_recording_id) + raise "You already have this JamKazam recording listed as a sample" if match + end + + def user_type_service_unique + match = PerformanceSample.exists?(:user_id => user.id, :service_id => self.service_id) + raise "You already have this #{self.type} sample listed (#{self.service_id}." if match + end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 30bde5246..a553e07d4 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -152,6 +152,9 @@ module JamRuby # This causes the authenticate method to be generated (among other stuff) #has_secure_password + has_many :user_presences, :class_name => "JamRuby::UserPresence" + has_many :performance_samples, :class_name => "JamRuby::PerformanceSample" + before_save :create_remember_token, :if => :should_validate_password? before_save :stringify_avatar_info , :if => :updating_avatar diff --git a/ruby/lib/jam_ruby/models/user_presence.rb b/ruby/lib/jam_ruby/models/user_presence.rb index d0274ef9a..1cf9ec3e4 100644 --- a/ruby/lib/jam_ruby/models/user_presence.rb +++ b/ruby/lib/jam_ruby/models/user_presence.rb @@ -1,4 +1,9 @@ module JamRuby class UserPresence < ActiveRecord::Base + belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id" + + def self.save(user, params) + UserPresence.create(:user => user, :type => params[:type], :username => username) + end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/user_spec.rb b/ruby/spec/jam_ruby/models/user_spec.rb index 6d51c9b6e..39e161db5 100644 --- a/ruby/spec/jam_ruby/models/user_spec.rb +++ b/ruby/spec/jam_ruby/models/user_spec.rb @@ -667,13 +667,16 @@ describe User do describe "age" do let(:user) {FactoryGirl.create(:user)} - + it "should calculate age based on birth_date" do user.birth_date = Time.now - 10.years user.age.should == 10 user.birth_date = Time.now - 10.years + 3.months user.age.should == 9 + + user.birth_date = nil + user.age.should == "unspecified" end end diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 7d8d40f94..7569c0b60 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -13,7 +13,7 @@ class ApiUsersController < ApiController :band_invitation_index, :band_invitation_show, :band_invitation_update, # band invitations :set_password, :begin_update_email, :update_avatar, :delete_avatar, :generate_filepicker_policy, :share_session, :share_recording, - :affiliate_report, :audio_latency] + :affiliate_report, :audio_latency, :profile] respond_to :json @@ -699,6 +699,9 @@ class ApiUsersController < ApiController end end + def profile + end + ###################### RECORDINGS ####################### # def recording_index # @recordings = User.recording_index(current_user, params[:id]) diff --git a/web/config/routes.rb b/web/config/routes.rb index 2a18c7fce..eabaef5e6 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -335,6 +335,9 @@ SampleApp::Application.routes.draw do match '/users/:id/share/session/:provider' => 'api_users#share_session', :via => :get match '/users/:id/share/recording/:provider' => 'api_users#share_recording', :via => :get + #profile + match '/users/:id/profile' => 'api_users#profile', :via => :get, :as => 'api_users_profile' + # session chat match '/chat' => 'api_chats#create', :via => :post match '/sessions/:music_session/chats' => 'api_chats#index', :via => :get From 79507f5f06fdb770e4e7318d99215dd53e9d417e Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sun, 1 Feb 2015 08:42:44 -0500 Subject: [PATCH 003/195] VRFS-2693 VRFS-2694 wip new musician profile models and specs --- .../lib/jam_ruby/models/performance_sample.rb | 28 +++++-- ruby/lib/jam_ruby/models/user_presence.rb | 20 ++++- .../models/performance_sample_spec.rb | 0 .../jam_ruby/models/user_presence_spec.rb | 73 +++++++++++++++++++ 4 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 ruby/spec/jam_ruby/models/performance_sample_spec.rb create mode 100644 ruby/spec/jam_ruby/models/user_presence_spec.rb diff --git a/ruby/lib/jam_ruby/models/performance_sample.rb b/ruby/lib/jam_ruby/models/performance_sample.rb index a406effbc..91959cf5d 100644 --- a/ruby/lib/jam_ruby/models/performance_sample.rb +++ b/ruby/lib/jam_ruby/models/performance_sample.rb @@ -1,19 +1,37 @@ module JamRuby class PerformanceSample < ActiveRecord::Base belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id" - belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording" :foreign_key => "claimed_recording_id" + belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id" - validates :user_type_recording_unique :if => lambda { |p| p.type == "jamkazam" } - validates :user_type_service_unique :if => lambda { |p| p.type != "jamkazam" } + validates :type, presence:true, length: {maximum: 100} + validates :username, presence:true, length: {maximum: 100} + validate :user_type_recording_unique, :if => lambda { |p| p.type == "jamkazam" } + validate :user_type_service_unique, :if => lambda { |p| p.type != "jamkazam" } def user_type_recording_unique match = PerformanceSample.exists?(:user_id => user.id, :claimed_recording_id => self.claimed_recording_id) - raise "You already have this JamKazam recording listed as a sample" if match + raise ConflictError, "You already have this JamKazam recording listed as a sample" if match end def user_type_service_unique match = PerformanceSample.exists?(:user_id => user.id, :service_id => self.service_id) - raise "You already have this #{self.type} sample listed (#{self.service_id}." if match + raise ConflictError, "You already have this #{self.type} sample listed (#{self.service_id}." if match + end + + def self.index(options = {}) + raise JamArgumentError, "The user is not specified." if options[:id].blank? + PerformanceSample.where("user_id = ?", options[:id]) + end + + def self.save(current_user, options = {}) + raise PermissionError, "You do not have permission to perform this operation" if current_user.nil? || options[:user_id] != current_user.id + PerformanceSample.create(:user => user, :type => options[:type], :username => options[:username]) + end + + def self.destroy(current_user, options = {}) + raise PermissionError, "You do not have permission to perform this operation" if current_user.nil? || options[:user_id] != current_user.id + raise JamArgumentError, "The performance sample ID is missing." if options[:id].blank? + PerformanceSample.destroy_all("id = ?", options[:id]) end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/user_presence.rb b/ruby/lib/jam_ruby/models/user_presence.rb index 1cf9ec3e4..23f10fe25 100644 --- a/ruby/lib/jam_ruby/models/user_presence.rb +++ b/ruby/lib/jam_ruby/models/user_presence.rb @@ -1,9 +1,25 @@ module JamRuby class UserPresence < ActiveRecord::Base + + attr_accessible :user_id, :type, :username belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id" - def self.save(user, params) - UserPresence.create(:user => user, :type => params[:type], :username => username) + validates :type, presence:true, length: {maximum: 100} + validates :username, presence:true, length: {maximum: 100} + + def self.index(options = {}) + raise JamArgumentError, "The user is not specified." if options[:id].blank? + UserPresence.where("user_id = ?", options[:id]) + end + + def self.save(current_user, options = {}) + raise PermissionError, "You do not have permission to perform this operation" if current_user.nil? || options[:user_id] != current_user.id + UserPresence.create(:user => current_user, :type => options[:type], :username => options[:username]) + end + + def self.destroy(current_user, options = {}) + raise PermissionError, "You do not have permission to perform this operation" if current_user.nil? || options[:user_id] != current_user.id + UserPresence.destroy_all("id = ?", options[:id]) end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/performance_sample_spec.rb b/ruby/spec/jam_ruby/models/performance_sample_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/ruby/spec/jam_ruby/models/user_presence_spec.rb b/ruby/spec/jam_ruby/models/user_presence_spec.rb new file mode 100644 index 000000000..342efc814 --- /dev/null +++ b/ruby/spec/jam_ruby/models/user_presence_spec.rb @@ -0,0 +1,73 @@ +require 'spec_helper' + +describe UserPresence do + + let(:user1) { FactoryGirl.create(:user) } + let(:user2) { FactoryGirl.create(:user) } + + describe "index" do + before(:all) do + @user1_presence1 = UserPresence.new(:user_id => user1.id, :username => "myonlineusername", :type => "facebook") + @user1_presence1.save! + + @user1_presence2 = UserPresence.new(:user_id => user1.id, :username => "myonlineusername", :type => "twitter") + @user1_presence2.save! + + @user2_presence1 = UserPresence.new(:user_id => user2.id, :username => "myonlineusername", :type => "soundcloud") + @user2_presence1.save! + end + + context "when request is valid" do + it "should return all records for user" do + presence = UserPresence.index({:id => user1.id}) + presence.count.should == 2 + + presence = UserPresence.index({:id => user2.id}) + presence.count.should == 1 + end + end + + context "when request is invalid" do + it "should raise error when options are missing" do + lambda{UserPresence.index}.should raise_error(JamArgumentError) + end + + it "should raise error when user id is missing" do + lambda{UserPresence.index({:id => ""})}.should raise_error(JamArgumentError) + end + end + end + + describe "save" do + context "when request is valid" do + it "should save successfully" do + end + end + + context "when request is not valid" do + it "should raise PermissionError if requester id does not match id in request" do + end + + it "should raise error if type is missing" do + end + + it "should raise error if username is missing" do + end + end + end + + describe "destroy" do + context "when request is valid" do + it "should destroy successfully" do + end + end + + context "when request is not valid" do + it "should raise PermissionError if requester id does not match id in request" do + end + + it "should raise error if user presence id is missing" do + end + end + end +end \ No newline at end of file From cce2ce2ffc9f95a0a86597285c2ec560c168fcfb Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sun, 1 Feb 2015 17:15:01 -0500 Subject: [PATCH 004/195] minor change to ensure current instance session id is used --- ruby/lib/jam_ruby/models/notification.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index e0ec6590b..b0cb8a3bb 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -57,7 +57,7 @@ module JamRuby # remove all notifications related to this session if it's not found if session.nil? - Notification.delete_all "(session_id = '#{session_id}')" + Notification.delete_all "(session_id = '#{self.session_id}')" end end From 05ba1ea5d36957d0fbd36df782562bf63e8090fb Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Tue, 3 Feb 2015 23:37:20 -0500 Subject: [PATCH 005/195] VRFS-2694 wip user presence model and spec --- ruby/lib/jam_ruby/models/user_presence.rb | 25 +++++++++++++---- .../jam_ruby/models/user_presence_spec.rb | 27 ++++++++++++++++++- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/ruby/lib/jam_ruby/models/user_presence.rb b/ruby/lib/jam_ruby/models/user_presence.rb index 23f10fe25..24cc23b1d 100644 --- a/ruby/lib/jam_ruby/models/user_presence.rb +++ b/ruby/lib/jam_ruby/models/user_presence.rb @@ -1,6 +1,8 @@ module JamRuby class UserPresence < ActiveRecord::Base + PERMISSION_MSG = "You do not have permission to perform this operation" + attr_accessible :user_id, :type, :username belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id" @@ -12,14 +14,27 @@ module JamRuby UserPresence.where("user_id = ?", options[:id]) end - def self.save(current_user, options = {}) - raise PermissionError, "You do not have permission to perform this operation" if current_user.nil? || options[:user_id] != current_user.id - UserPresence.create(:user => current_user, :type => options[:type], :username => options[:username]) + def self.create(current_user, options = {}) + raise PermissionError, PERMISSION_MSG if current_user.nil? || options[:user_id] != current_user.id + raise StateError, "Missing required information" if options[:type].nil? || options[:username].nil? + + u = UserPresence.new({:user_id => current_user.id, :type => options[:type], :username => options[:username]}) + u.save! + end + + def self.update(current_user, options = {}) + raise PermissionError, PERMISSION_MSG if current_user.nil? || options[:user_id] != current_user.id end def self.destroy(current_user, options = {}) - raise PermissionError, "You do not have permission to perform this operation" if current_user.nil? || options[:user_id] != current_user.id - UserPresence.destroy_all("id = ?", options[:id]) + raise PermissionError, PERMISSION_MSG if current_user.nil? || options[:user_id] != current_user.id + + id = options[:id] + user_presence = UserPresence.find(user_presence) + unless user_presence.nil? + raise PermissionError, PERMISSION_MSG if user_presence.user_id != current_user.id + UserPresence.destroy(id) + end end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/user_presence_spec.rb b/ruby/spec/jam_ruby/models/user_presence_spec.rb index 342efc814..7ce7e22c7 100644 --- a/ruby/spec/jam_ruby/models/user_presence_spec.rb +++ b/ruby/spec/jam_ruby/models/user_presence_spec.rb @@ -38,7 +38,32 @@ describe UserPresence do end end - describe "save" do + describe "create" do + context "when request is valid" do + it "should save successfully" do + UserPresence.create(user1, {:user_id => user1.id, :type => "soundcloud", :username => "soundclouduser1"}) + + # make sure we can save a second UserPresence for same user and type + UserPresence.create(user1, {:user_id => user1.id, :type => "soundcloud", :username => "soundclouduser2"}) + end + end + + context "when request is not valid" do + it "should raise PermissionError if requester id does not match id in request" do + lambda{UserPresence.create(user1, {:user_id => user2.id, :type => "soundcloud", :username => "soundclouduser2"})}.should raise_error(PermissionError) + end + + it "should raise error if type is missing" do + lambda{UserPresence.create(user1, {:user_id => user1.id, :username => "soundclouduser2"})}.should raise_error(StateError) + end + + it "should raise error if username is missing" do + lambda{UserPresence.create(user1, {:user_id => user1.id, :type => "soundcloud"})}.should raise_error(StateError) + end + end + end + + describe "update" do context "when request is valid" do it "should save successfully" do end From 33aef8a38031d3184fc5b4344f8ab5fe21510503 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 5 Feb 2015 03:34:25 -0500 Subject: [PATCH 006/195] VRFS-2694 user presence model --- db/manifest | 3 +- db/up/alter_type_columns.sql | 2 ++ ruby/lib/jam_ruby/models/user_presence.rb | 32 ++++++++++++------ .../jam_ruby/models/user_presence_spec.rb | 33 +++++++++++++++---- 4 files changed, 52 insertions(+), 18 deletions(-) create mode 100644 db/up/alter_type_columns.sql diff --git a/db/manifest b/db/manifest index fdfec768a..6227fe0e1 100755 --- a/db/manifest +++ b/db/manifest @@ -246,4 +246,5 @@ text_message_migration.sql user_model_about_changes.sql performance_samples.sql user_presences.sql -discard_scores_optimized.sql \ No newline at end of file +discard_scores_optimized.sql +alter_type_columns.sql \ No newline at end of file diff --git a/db/up/alter_type_columns.sql b/db/up/alter_type_columns.sql new file mode 100644 index 000000000..690edec52 --- /dev/null +++ b/db/up/alter_type_columns.sql @@ -0,0 +1,2 @@ +ALTER TABLE user_presences RENAME COLUMN type to service_type; +ALTER TABLE performance_samples RENAME COLUMN type to service_type; \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/user_presence.rb b/ruby/lib/jam_ruby/models/user_presence.rb index 24cc23b1d..e4b6cce03 100644 --- a/ruby/lib/jam_ruby/models/user_presence.rb +++ b/ruby/lib/jam_ruby/models/user_presence.rb @@ -3,10 +3,10 @@ module JamRuby PERMISSION_MSG = "You do not have permission to perform this operation" - attr_accessible :user_id, :type, :username + attr_accessible :user_id, :service_type, :username belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id" - validates :type, presence:true, length: {maximum: 100} + validates :service_type, presence:true, length: {maximum: 100} validates :username, presence:true, length: {maximum: 100} def self.index(options = {}) @@ -15,26 +15,38 @@ module JamRuby end def self.create(current_user, options = {}) - raise PermissionError, PERMISSION_MSG if current_user.nil? || options[:user_id] != current_user.id - raise StateError, "Missing required information" if options[:type].nil? || options[:username].nil? + auth_user(current_user, options) + raise StateError, "Missing required information" if options[:service_type].nil? || options[:username].nil? - u = UserPresence.new({:user_id => current_user.id, :type => options[:type], :username => options[:username]}) - u.save! + up = UserPresence.new({:user_id => current_user.id, :service_type => options[:service_type], :username => options[:username]}) + up.save! end def self.update(current_user, options = {}) - raise PermissionError, PERMISSION_MSG if current_user.nil? || options[:user_id] != current_user.id + auth_user(current_user, options) + + id = options[:id] + if options[:username].nil? + UserPresence.destroy(id) + else + up = UserPresence.find(options[:id]) + up.service_type = options[:service_type] + up.username = options[:username] + up.save! + end end def self.destroy(current_user, options = {}) - raise PermissionError, PERMISSION_MSG if current_user.nil? || options[:user_id] != current_user.id - - id = options[:id] user_presence = UserPresence.find(user_presence) unless user_presence.nil? raise PermissionError, PERMISSION_MSG if user_presence.user_id != current_user.id UserPresence.destroy(id) end end + + private + def self.auth_user(current_user, options={}) + raise PermissionError, PERMISSION_MSG if current_user.nil? || options[:user_id] != current_user.id + end end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/user_presence_spec.rb b/ruby/spec/jam_ruby/models/user_presence_spec.rb index 7ce7e22c7..e92f28ff3 100644 --- a/ruby/spec/jam_ruby/models/user_presence_spec.rb +++ b/ruby/spec/jam_ruby/models/user_presence_spec.rb @@ -6,14 +6,15 @@ describe UserPresence do let(:user2) { FactoryGirl.create(:user) } describe "index" do + before(:all) do - @user1_presence1 = UserPresence.new(:user_id => user1.id, :username => "myonlineusername", :type => "facebook") + @user1_presence1 = UserPresence.new(:user_id => user1.id, :username => "myonlineusername", :service_type => "facebook") @user1_presence1.save! - @user1_presence2 = UserPresence.new(:user_id => user1.id, :username => "myonlineusername", :type => "twitter") + @user1_presence2 = UserPresence.new(:user_id => user1.id, :username => "myonlineusername", :service_type => "twitter") @user1_presence2.save! - @user2_presence1 = UserPresence.new(:user_id => user2.id, :username => "myonlineusername", :type => "soundcloud") + @user2_presence1 = UserPresence.new(:user_id => user2.id, :username => "myonlineusername", :service_type => "soundcloud") @user2_presence1.save! end @@ -41,16 +42,16 @@ describe UserPresence do describe "create" do context "when request is valid" do it "should save successfully" do - UserPresence.create(user1, {:user_id => user1.id, :type => "soundcloud", :username => "soundclouduser1"}) + UserPresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1"}) # make sure we can save a second UserPresence for same user and type - UserPresence.create(user1, {:user_id => user1.id, :type => "soundcloud", :username => "soundclouduser2"}) + UserPresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser2"}) end end context "when request is not valid" do it "should raise PermissionError if requester id does not match id in request" do - lambda{UserPresence.create(user1, {:user_id => user2.id, :type => "soundcloud", :username => "soundclouduser2"})}.should raise_error(PermissionError) + lambda{UserPresence.create(user1, {:user_id => user2.id, :service_type => "soundcloud", :username => "soundclouduser2"})}.should raise_error(PermissionError) end it "should raise error if type is missing" do @@ -58,14 +59,32 @@ describe UserPresence do end it "should raise error if username is missing" do - lambda{UserPresence.create(user1, {:user_id => user1.id, :type => "soundcloud"})}.should raise_error(StateError) + lambda{UserPresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud"})}.should raise_error(StateError) end end end describe "update" do + + before(:all) do + @user_presence = UserPresence.new(:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1") + @user_presence.save! + end + context "when request is valid" do it "should save successfully" do + + up_list = UserPresence.index({:id => user1.id}) + up_list.count.should == 1 + up_list.first.service_type.should == "soundcloud" + up_list.first.username.should == "soundclouduser1" + + UserPresence.update(user1, {:id => @user_presence.id, :user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser2"}) + + up_list = UserPresence.index({:id => user1.id}) + up_list.count.should == 1 + up_list.first.service_type.should == "soundcloud" + up_list.first.username.should == "soundclouduser2" end end From e3f27eeebcc11756564ae59c81bfd80b706326a5 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Tue, 10 Feb 2015 02:44:16 -0500 Subject: [PATCH 007/195] VRFS-2694 user presence model tests --- ruby/lib/jam_ruby/models/user_presence.rb | 28 ++++++++++--------- .../jam_ruby/models/user_presence_spec.rb | 24 ++++++++++++++++ 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/ruby/lib/jam_ruby/models/user_presence.rb b/ruby/lib/jam_ruby/models/user_presence.rb index e4b6cce03..a225039ed 100644 --- a/ruby/lib/jam_ruby/models/user_presence.rb +++ b/ruby/lib/jam_ruby/models/user_presence.rb @@ -16,7 +16,7 @@ module JamRuby def self.create(current_user, options = {}) auth_user(current_user, options) - raise StateError, "Missing required information" if options[:service_type].nil? || options[:username].nil? + raise StateError, "Missing required information" if options[:service_type].blank? || options[:username].blank? up = UserPresence.new({:user_id => current_user.id, :service_type => options[:service_type], :username => options[:username]}) up.save! @@ -24,22 +24,24 @@ module JamRuby def self.update(current_user, options = {}) auth_user(current_user, options) + raise StateError, "Missing required information" if options[:service_type].blank? || options[:username].blank? || options[:id].blank? - id = options[:id] - if options[:username].nil? - UserPresence.destroy(id) - else - up = UserPresence.find(options[:id]) - up.service_type = options[:service_type] - up.username = options[:username] - up.save! - end + up = UserPresence.find(options[:id]) + up.service_type = options[:service_type] + up.username = options[:username] + up.save! end - def self.destroy(current_user, options = {}) - user_presence = UserPresence.find(user_presence) + def self.delete(current_user, options = {}) + id = options[:id] + raise StateError, "Missing required information" if id.blank? + user_presence = UserPresence.find(id) + + if user_presence.user_id != current_user.id + raise PermissionError, PERMISSION_MSG + end + unless user_presence.nil? - raise PermissionError, PERMISSION_MSG if user_presence.user_id != current_user.id UserPresence.destroy(id) end end diff --git a/ruby/spec/jam_ruby/models/user_presence_spec.rb b/ruby/spec/jam_ruby/models/user_presence_spec.rb index e92f28ff3..a3a8fc43d 100644 --- a/ruby/spec/jam_ruby/models/user_presence_spec.rb +++ b/ruby/spec/jam_ruby/models/user_presence_spec.rb @@ -90,27 +90,51 @@ describe UserPresence do context "when request is not valid" do it "should raise PermissionError if requester id does not match id in request" do + lambda{UserPresence.update(user1, {:user_id => user2.id, :id => @user_presence.id, :service_type => "soundcloud", :username => "soundclouduser2"})}.should raise_error(PermissionError) end it "should raise error if type is missing" do + lambda{UserPresence.update(user1, {:user_id => user1.id, :id => @user_presence.id, :username => "soundclouduser2"})}.should raise_error(StateError) end it "should raise error if username is missing" do + lambda{UserPresence.update(user1, {:user_id => user1.id, :id => @user_presence.id, :service_type => "soundcloud"})}.should raise_error(StateError) + end + + it "should raise error if user presence id is missing" do + lambda{UserPresence.update(user1, {:user_id => user1.id, :username => "soundclouduser2", :service_type => "soundcloud"})}.should raise_error(StateError) end end end describe "destroy" do + + before(:all) do + @user_presence = UserPresence.new(:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1") + @user_presence.save! + end + context "when request is valid" do it "should destroy successfully" do + up_list = UserPresence.index({:id => user1.id}) + up_list.count.should == 1 + up_list.first.service_type.should == "soundcloud" + up_list.first.username.should == "soundclouduser1" + + UserPresence.delete(user1, {:user_id => user1.id, :id => @user_presence.id}) + + up_list = UserPresence.index({:id => user1.id}) + up_list.count.should == 0 end end context "when request is not valid" do it "should raise PermissionError if requester id does not match id in request" do + lambda{UserPresence.delete(user2, {:user_id => user1.id, :id => @user_presence.id})}.should raise_error(PermissionError) end it "should raise error if user presence id is missing" do + lambda{UserPresence.delete(user1, {:user_id => user1.id})}.should raise_error(StateError) end end end From 895fb92365dd80f93a55280b36adf5ad3b9a0347 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 11 Feb 2015 22:33:10 -0500 Subject: [PATCH 008/195] VRFS-2693 VRFS-2694 performance sample model wip --- ruby/lib/jam_ruby/models/claimed_recording.rb | 1 + .../lib/jam_ruby/models/performance_sample.rb | 65 ++++++-- ruby/lib/jam_ruby/models/user_presence.rb | 9 +- .../models/performance_sample_spec.rb | 141 ++++++++++++++++++ .../jam_ruby/models/user_presence_spec.rb | 33 +++- 5 files changed, 231 insertions(+), 18 deletions(-) diff --git a/ruby/lib/jam_ruby/models/claimed_recording.rb b/ruby/lib/jam_ruby/models/claimed_recording.rb index 88898cb89..7aa73440a 100644 --- a/ruby/lib/jam_ruby/models/claimed_recording.rb +++ b/ruby/lib/jam_ruby/models/claimed_recording.rb @@ -12,6 +12,7 @@ module JamRuby has_many :playing_sessions, :class_name => "JamRuby::ActiveMusicSession" has_many :likes, :class_name => "JamRuby::RecordingLiker", :foreign_key => "claimed_recording_id", :dependent => :destroy has_many :plays, :class_name => "JamRuby::PlayablePlay", :foreign_key => "claimed_recording_id", :dependent => :destroy + has_many :performance_samples, :class_name => "JamRuby::PerformanceSample", :foreign_key => "claimed_recording_id", :dependent => :destroy has_one :share_token, :class_name => "JamRuby::ShareToken", :inverse_of => :shareable, :foreign_key => 'shareable_id', :dependent => :destroy validates :name, no_profanity: true, length: {minimum: 3, maximum: 64}, presence: true diff --git a/ruby/lib/jam_ruby/models/performance_sample.rb b/ruby/lib/jam_ruby/models/performance_sample.rb index 91959cf5d..163b19dc5 100644 --- a/ruby/lib/jam_ruby/models/performance_sample.rb +++ b/ruby/lib/jam_ruby/models/performance_sample.rb @@ -1,21 +1,39 @@ module JamRuby class PerformanceSample < ActiveRecord::Base + + PERMISSION_MSG = "You do not have permission to perform this operation." + + attr_accessible :user_id, :service_type, :claimed_recording_id, :service_id, :url + belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id" belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id" - validates :type, presence:true, length: {maximum: 100} - validates :username, presence:true, length: {maximum: 100} - validate :user_type_recording_unique, :if => lambda { |p| p.type == "jamkazam" } - validate :user_type_service_unique, :if => lambda { |p| p.type != "jamkazam" } + validates :service_type, presence:true, length: {maximum: 100} + + # JamKazam validators + validate :claimed_recording_id_present, :if => lambda { |p| p.service_type == "jamkazam" } + validate :user_type_recording_unique, :if => lambda { |p| p.service_type == "jamkazam" } + + # Non-JamKazam validators + validate :service_id_present, :if => lambda { |p| p.service_type != "jamkazam" } + validate :user_type_service_unique, :if => lambda { |p| p.service_type != "jamkazam" } + + def claimed_recording_id_present + raise StateError, "Claimed recording is required for JamKazam performance samples" if self.claimed_recording_id.blank? + end def user_type_recording_unique - match = PerformanceSample.exists?(:user_id => user.id, :claimed_recording_id => self.claimed_recording_id) + match = PerformanceSample.exists?(:user_id => self.user_id, :claimed_recording_id => self.claimed_recording_id, :service_type => self.service_type) raise ConflictError, "You already have this JamKazam recording listed as a sample" if match end + def service_id_present + raise StateError, "Service ID is required for non-JamKazam performance samples" if self.service_id.blank? + end + def user_type_service_unique - match = PerformanceSample.exists?(:user_id => user.id, :service_id => self.service_id) - raise ConflictError, "You already have this #{self.type} sample listed (#{self.service_id}." if match + match = PerformanceSample.exists?(:user_id => self.user_id, :service_id => self.service_id, :service_type => self.service_type) + raise ConflictError, "You already have this #{self.service_type} sample listed (#{self.service_id})." if match end def self.index(options = {}) @@ -23,9 +41,31 @@ module JamRuby PerformanceSample.where("user_id = ?", options[:id]) end - def self.save(current_user, options = {}) - raise PermissionError, "You do not have permission to perform this operation" if current_user.nil? || options[:user_id] != current_user.id - PerformanceSample.create(:user => user, :type => options[:type], :username => options[:username]) + def self.create(current_user, options = {}) + auth_user(current_user, options) + raise StateError, "Missing required information" if options[:service_type].blank? + + ps = PerformanceSample.new({ + :user_id => current_user.id, + :service_type => options[:service_type], + :claimed_recording_id => options[:claimed_recording_id], + :service_id => options[:service_id], + :url => options[:url] + }) + + ps.save! + end + + def self.update(current_user, options = {}) + auth_user(current_user, options) + raise StateError, "Missing required information" if options[:service_type].blank? || options[:username].blank? || options[:id].blank? + + ps = PerformanceSample.find(options[:id]) + ps.service_type = options[:service_type] + ps.claimed_recording_id = options[:claimed_recording_id] + ps.service_id = options[:service_id] + ps.url = options[:url] + ps.save! end def self.destroy(current_user, options = {}) @@ -33,5 +73,10 @@ module JamRuby raise JamArgumentError, "The performance sample ID is missing." if options[:id].blank? PerformanceSample.destroy_all("id = ?", options[:id]) end + + private + def self.auth_user(current_user, options={}) + raise PermissionError, PERMISSION_MSG if current_user.nil? || options[:user_id] != current_user.id + end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/user_presence.rb b/ruby/lib/jam_ruby/models/user_presence.rb index a225039ed..6484c398d 100644 --- a/ruby/lib/jam_ruby/models/user_presence.rb +++ b/ruby/lib/jam_ruby/models/user_presence.rb @@ -1,7 +1,7 @@ module JamRuby class UserPresence < ActiveRecord::Base - PERMISSION_MSG = "You do not have permission to perform this operation" + PERMISSION_MSG = "You do not have permission to perform this operation." attr_accessible :user_id, :service_type, :username belongs_to :user, :class_name => "JamRuby::User", :foreign_key => "user_id" @@ -9,6 +9,13 @@ module JamRuby validates :service_type, presence:true, length: {maximum: 100} validates :username, presence:true, length: {maximum: 100} + validate :username_service_type_unique + + def username_service_type_unique + match = UserPresence.exists?(:username => self.username, :service_type => self.service_type) + raise ConflictError, "Username #{self.username} is already associated with a #{self.service_type} account" if match + end + def self.index(options = {}) raise JamArgumentError, "The user is not specified." if options[:id].blank? UserPresence.where("user_id = ?", options[:id]) diff --git a/ruby/spec/jam_ruby/models/performance_sample_spec.rb b/ruby/spec/jam_ruby/models/performance_sample_spec.rb index e69de29bb..c82b77128 100644 --- a/ruby/spec/jam_ruby/models/performance_sample_spec.rb +++ b/ruby/spec/jam_ruby/models/performance_sample_spec.rb @@ -0,0 +1,141 @@ +require 'spec_helper' + +describe PerformanceSample do + + let(:user1) { FactoryGirl.create(:user) } + let(:user2) { FactoryGirl.create(:user) } + let(:claimed_recording) { FactoryGirl.create(:claimed_recording) } + + describe "index" do + + before(:all) do + + @user1_sample1 = PerformanceSample.new(:user_id => user1.id, :service_type => "jamkazam", :claimed_recording_id => claimed_recording.id) + @user1_sample1.save! + + @user1_sample2 = PerformanceSample.new(:user_id => user1.id, :service_type => "youtube", :service_id => "12345") + @user1_sample2.save! + + @user2_sample1 = PerformanceSample.new(:user_id => user2.id, :service_type => "soundcloud", :service_id => "67890") + @user2_sample1.save! + end + + context "when request is valid" do + it "should return all records for user" do + sample = PerformanceSample.index({:id => user1.id}) + sample.count.should == 2 + + sample = PerformanceSample.index({:id => user2.id}) + sample.count.should == 1 + end + end + + context "when request is invalid" do + it "should raise error when options are missing" do + lambda{PerformanceSample.index}.should raise_error(JamArgumentError) + end + + it "should raise error when user id is missing" do + lambda{PerformanceSample.index({:id => ""})}.should raise_error(JamArgumentError) + end + end + end + + describe "create" do + context "when request is valid" do + it "should save successfully" do + PerformanceSample.create(user1, {:user_id => user1.id, :service_type => "youtube", :service_id => "12345"}) + + # make sure we can save a second PerformanceSample for same user and type + PerformanceSample.create(user1, {:user_id => user1.id, :service_type => "youtube", :service_id => "67890"}) + end + end + + context "when request is not valid" do + it "should raise PermissionError if requester id does not match id in request" do + lambda{PerformanceSample.create(user1, {:user_id => user2.id, :service_type => "soundcloud", :username => "soundclouduser2"})}.should raise_error(PermissionError) + end + + it "should raise error if service type is missing" do + lambda{PerformanceSample.create(user1, {:user_id => user1.id, :username => "soundclouduser2"})}.should raise_error(StateError) + end + + + end + end + + # describe "update" do + + # before(:all) do + # @user_sample = PerformanceSample.new(:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1") + # @user_sample.save! + # end + + # context "when request is valid" do + # it "should save successfully" do + + # up_list = PerformanceSample.index({:id => user1.id}) + # up_list.count.should == 1 + # up_list.first.service_type.should == "soundcloud" + # up_list.first.username.should == "soundclouduser1" + + # PerformanceSample.update(user1, {:id => @user_sample.id, :user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser2"}) + + # up_list = PerformanceSample.index({:id => user1.id}) + # up_list.count.should == 1 + # up_list.first.service_type.should == "soundcloud" + # up_list.first.username.should == "soundclouduser2" + # end + # end + + # context "when request is not valid" do + # it "should raise PermissionError if requester id does not match id in request" do + # lambda{PerformanceSample.update(user1, {:user_id => user2.id, :id => @user_sample.id, :service_type => "soundcloud", :username => "soundclouduser2"})}.should raise_error(PermissionError) + # end + + # it "should raise error if type is missing" do + # lambda{PerformanceSample.update(user1, {:user_id => user1.id, :id => @user_sample.id, :username => "soundclouduser2"})}.should raise_error(StateError) + # end + + # it "should raise error if username is missing" do + # lambda{PerformanceSample.update(user1, {:user_id => user1.id, :id => @user_sample.id, :service_type => "soundcloud"})}.should raise_error(StateError) + # end + + # it "should raise error if user sample id is missing" do + # lambda{PerformanceSample.update(user1, {:user_id => user1.id, :username => "soundclouduser2", :service_type => "soundcloud"})}.should raise_error(StateError) + # end + # end + # end + + # describe "destroy" do + + # before(:all) do + # @user_sample = PerformanceSample.new(:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1") + # @user_sample.save! + # end + + # context "when request is valid" do + # it "should destroy successfully" do + # up_list = PerformanceSample.index({:id => user1.id}) + # up_list.count.should == 1 + # up_list.first.service_type.should == "soundcloud" + # up_list.first.username.should == "soundclouduser1" + + # PerformanceSample.delete(user1, {:user_id => user1.id, :id => @user_sample.id}) + + # up_list = PerformanceSample.index({:id => user1.id}) + # up_list.count.should == 0 + # end + # end + + # context "when request is not valid" do + # it "should raise PermissionError if requester id does not match id in request" do + # lambda{PerformanceSample.delete(user2, {:user_id => user1.id, :id => @user_sample.id})}.should raise_error(PermissionError) + # end + + # it "should raise error if user sample id is missing" do + # lambda{PerformanceSample.delete(user1, {:user_id => user1.id})}.should raise_error(StateError) + # end + # end + # end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/user_presence_spec.rb b/ruby/spec/jam_ruby/models/user_presence_spec.rb index a3a8fc43d..815fba50e 100644 --- a/ruby/spec/jam_ruby/models/user_presence_spec.rb +++ b/ruby/spec/jam_ruby/models/user_presence_spec.rb @@ -8,14 +8,16 @@ describe UserPresence do describe "index" do before(:all) do - @user1_presence1 = UserPresence.new(:user_id => user1.id, :username => "myonlineusername", :service_type => "facebook") - @user1_presence1.save! + UserPresence.delete_all - @user1_presence2 = UserPresence.new(:user_id => user1.id, :username => "myonlineusername", :service_type => "twitter") - @user1_presence2.save! + user1_presence1 = UserPresence.new({:user_id => user1.id, :username => "myonlineusername", :service_type => "facebook"}) + user1_presence1.save! - @user2_presence1 = UserPresence.new(:user_id => user2.id, :username => "myonlineusername", :service_type => "soundcloud") - @user2_presence1.save! + user1_presence2 = UserPresence.new({:user_id => user1.id, :username => "myonlineusername", :service_type => "twitter"}) + user1_presence2.save! + + user2_presence1 = UserPresence.new({:user_id => user2.id, :username => "myonlineusername", :service_type => "soundcloud"}) + user2_presence1.save! end context "when request is valid" do @@ -40,12 +42,19 @@ describe UserPresence do end describe "create" do + + before(:all) do + UserPresence.delete_all + end + context "when request is valid" do it "should save successfully" do UserPresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1"}) # make sure we can save a second UserPresence for same user and type UserPresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser2"}) + + UserPresence.index({:id => user1.id}).count.should == 2 end end @@ -54,19 +63,28 @@ describe UserPresence do lambda{UserPresence.create(user1, {:user_id => user2.id, :service_type => "soundcloud", :username => "soundclouduser2"})}.should raise_error(PermissionError) end - it "should raise error if type is missing" do + it "should raise error if service type is missing" do lambda{UserPresence.create(user1, {:user_id => user1.id, :username => "soundclouduser2"})}.should raise_error(StateError) end it "should raise error if username is missing" do lambda{UserPresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud"})}.should raise_error(StateError) end + + it "should not allow duplicates of the same username / service type combination" do + UserPresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1"}) + UserPresence.index({:id => user1.id}).count.should == 1 + + lambda{UserPresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1"})}.should raise_error(ConflictError) + UserPresence.index({:id => user1.id}).count.should == 1 + end end end describe "update" do before(:all) do + UserPresence.delete_all @user_presence = UserPresence.new(:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1") @user_presence.save! end @@ -110,6 +128,7 @@ describe UserPresence do describe "destroy" do before(:all) do + UserPresence.delete_all @user_presence = UserPresence.new(:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1") @user_presence.save! end From 8bd311647a964b97f57a6e6c56b366eac2f6bc03 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Fri, 13 Feb 2015 01:32:55 -0500 Subject: [PATCH 009/195] VRFS-2693 performance model/tests --- .../lib/jam_ruby/models/performance_sample.rb | 18 +-- ruby/lib/jam_ruby/models/user_presence.rb | 2 +- .../models/performance_sample_spec.rb | 121 ++++++++---------- .../jam_ruby/models/user_presence_spec.rb | 4 +- 4 files changed, 57 insertions(+), 88 deletions(-) diff --git a/ruby/lib/jam_ruby/models/performance_sample.rb b/ruby/lib/jam_ruby/models/performance_sample.rb index 163b19dc5..df405275d 100644 --- a/ruby/lib/jam_ruby/models/performance_sample.rb +++ b/ruby/lib/jam_ruby/models/performance_sample.rb @@ -56,22 +56,10 @@ module JamRuby ps.save! end - def self.update(current_user, options = {}) - auth_user(current_user, options) - raise StateError, "Missing required information" if options[:service_type].blank? || options[:username].blank? || options[:id].blank? - - ps = PerformanceSample.find(options[:id]) - ps.service_type = options[:service_type] - ps.claimed_recording_id = options[:claimed_recording_id] - ps.service_id = options[:service_id] - ps.url = options[:url] - ps.save! - end - - def self.destroy(current_user, options = {}) + def self.delete(current_user, options = {}) raise PermissionError, "You do not have permission to perform this operation" if current_user.nil? || options[:user_id] != current_user.id - raise JamArgumentError, "The performance sample ID is missing." if options[:id].blank? - PerformanceSample.destroy_all("id = ?", options[:id]) + raise StateError, "The performance sample ID is missing." if options[:id].blank? + PerformanceSample.destroy(options[:id]) end private diff --git a/ruby/lib/jam_ruby/models/user_presence.rb b/ruby/lib/jam_ruby/models/user_presence.rb index 6484c398d..5f590e429 100644 --- a/ruby/lib/jam_ruby/models/user_presence.rb +++ b/ruby/lib/jam_ruby/models/user_presence.rb @@ -17,7 +17,7 @@ module JamRuby end def self.index(options = {}) - raise JamArgumentError, "The user is not specified." if options[:id].blank? + raise StateError, "The user is not specified." if options[:id].blank? UserPresence.where("user_id = ?", options[:id]) end diff --git a/ruby/spec/jam_ruby/models/performance_sample_spec.rb b/ruby/spec/jam_ruby/models/performance_sample_spec.rb index c82b77128..343afd4bd 100644 --- a/ruby/spec/jam_ruby/models/performance_sample_spec.rb +++ b/ruby/spec/jam_ruby/models/performance_sample_spec.rb @@ -9,6 +9,7 @@ describe PerformanceSample do describe "index" do before(:all) do + PerformanceSample.delete_all @user1_sample1 = PerformanceSample.new(:user_id => user1.id, :service_type => "jamkazam", :claimed_recording_id => claimed_recording.id) @user1_sample1.save! @@ -42,6 +43,11 @@ describe PerformanceSample do end describe "create" do + + before(:all) do + PerformanceSample.delete_all + end + context "when request is valid" do it "should save successfully" do PerformanceSample.create(user1, {:user_id => user1.id, :service_type => "youtube", :service_id => "12345"}) @@ -53,89 +59,64 @@ describe PerformanceSample do context "when request is not valid" do it "should raise PermissionError if requester id does not match id in request" do - lambda{PerformanceSample.create(user1, {:user_id => user2.id, :service_type => "soundcloud", :username => "soundclouduser2"})}.should raise_error(PermissionError) + lambda{PerformanceSample.create(user1, {:user_id => user2.id, :service_type => "soundcloud", :service_id => "12345"})}.should raise_error(PermissionError) end it "should raise error if service type is missing" do - lambda{PerformanceSample.create(user1, {:user_id => user1.id, :username => "soundclouduser2"})}.should raise_error(StateError) + lambda{PerformanceSample.create(user1, {:user_id => user1.id, :service_id => "12345"})}.should raise_error(StateError) end - + it "should raise error if service id is missing for non-JamKazam sample" do + lambda{PerformanceSample.create(user1, {:user_id => user1.id, :service_type => "youtube"})}.should raise_error(StateError) + end + + it "should raise error if recording id is missing for JamKazam sample" do + lambda{PerformanceSample.create(user1, {:user_id => user1.id, :service_type => "jamkazam"})}.should raise_error(StateError) + end + + it "should not allow duplicate type/service id combination for non-JamKazam sample" do + PerformanceSample.create(user1, {:user_id => user1.id, :service_type => "youtube", :service_id => "12345"}) + lambda{PerformanceSample.create(user1, {:user_id => user1.id, :service_type => "youtube", :service_id => "12345"})}.should raise_error(ConflictError) + end + + it "should not allow duplicate type/recording id combination for JamKazam sample" do + PerformanceSample.create(user1, {:user_id => user1.id, :service_type => "jamkazam", :claimed_recording_id => claimed_recording.id}) + lambda{PerformanceSample.create(user1, {:user_id => user1.id, :service_type => "jamkazam", :claimed_recording_id => claimed_recording.id})}.should raise_error(ConflictError) + end end end - # describe "update" do - # before(:all) do - # @user_sample = PerformanceSample.new(:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1") - # @user_sample.save! - # end + describe "destroy" do - # context "when request is valid" do - # it "should save successfully" do + before(:all) do + PerformanceSample.delete_all + @user_sample = PerformanceSample.new(:user_id => user1.id, :service_type => "soundcloud", :service_id => "12345") + @user_sample.save! + end - # up_list = PerformanceSample.index({:id => user1.id}) - # up_list.count.should == 1 - # up_list.first.service_type.should == "soundcloud" - # up_list.first.username.should == "soundclouduser1" + context "when request is valid" do + it "should destroy successfully" do + ps_list = PerformanceSample.index({:id => user1.id}) + ps_list.count.should == 1 + ps_list.first.service_type.should == "soundcloud" + ps_list.first.service_id.should == "12345" - # PerformanceSample.update(user1, {:id => @user_sample.id, :user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser2"}) + PerformanceSample.delete(user1, {:user_id => user1.id, :id => @user_sample.id}) - # up_list = PerformanceSample.index({:id => user1.id}) - # up_list.count.should == 1 - # up_list.first.service_type.should == "soundcloud" - # up_list.first.username.should == "soundclouduser2" - # end - # end + ps_list = PerformanceSample.index({:id => user1.id}) + ps_list.count.should == 0 + end + end - # context "when request is not valid" do - # it "should raise PermissionError if requester id does not match id in request" do - # lambda{PerformanceSample.update(user1, {:user_id => user2.id, :id => @user_sample.id, :service_type => "soundcloud", :username => "soundclouduser2"})}.should raise_error(PermissionError) - # end + context "when request is not valid" do + it "should raise PermissionError if requester id does not match id in request" do + lambda{PerformanceSample.delete(user2, {:user_id => user1.id, :id => @user_sample.id})}.should raise_error(PermissionError) + end - # it "should raise error if type is missing" do - # lambda{PerformanceSample.update(user1, {:user_id => user1.id, :id => @user_sample.id, :username => "soundclouduser2"})}.should raise_error(StateError) - # end - - # it "should raise error if username is missing" do - # lambda{PerformanceSample.update(user1, {:user_id => user1.id, :id => @user_sample.id, :service_type => "soundcloud"})}.should raise_error(StateError) - # end - - # it "should raise error if user sample id is missing" do - # lambda{PerformanceSample.update(user1, {:user_id => user1.id, :username => "soundclouduser2", :service_type => "soundcloud"})}.should raise_error(StateError) - # end - # end - # end - - # describe "destroy" do - - # before(:all) do - # @user_sample = PerformanceSample.new(:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1") - # @user_sample.save! - # end - - # context "when request is valid" do - # it "should destroy successfully" do - # up_list = PerformanceSample.index({:id => user1.id}) - # up_list.count.should == 1 - # up_list.first.service_type.should == "soundcloud" - # up_list.first.username.should == "soundclouduser1" - - # PerformanceSample.delete(user1, {:user_id => user1.id, :id => @user_sample.id}) - - # up_list = PerformanceSample.index({:id => user1.id}) - # up_list.count.should == 0 - # end - # end - - # context "when request is not valid" do - # it "should raise PermissionError if requester id does not match id in request" do - # lambda{PerformanceSample.delete(user2, {:user_id => user1.id, :id => @user_sample.id})}.should raise_error(PermissionError) - # end - - # it "should raise error if user sample id is missing" do - # lambda{PerformanceSample.delete(user1, {:user_id => user1.id})}.should raise_error(StateError) - # end - # end - # end + it "should raise error if user sample id is missing" do + lambda{PerformanceSample.delete(user1, {:user_id => user1.id})}.should raise_error(StateError) + end + end + end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/user_presence_spec.rb b/ruby/spec/jam_ruby/models/user_presence_spec.rb index 815fba50e..06f51221a 100644 --- a/ruby/spec/jam_ruby/models/user_presence_spec.rb +++ b/ruby/spec/jam_ruby/models/user_presence_spec.rb @@ -32,11 +32,11 @@ describe UserPresence do context "when request is invalid" do it "should raise error when options are missing" do - lambda{UserPresence.index}.should raise_error(JamArgumentError) + lambda{UserPresence.index}.should raise_error(StateError) end it "should raise error when user id is missing" do - lambda{UserPresence.index({:id => ""})}.should raise_error(JamArgumentError) + lambda{UserPresence.index({:id => ""})}.should raise_error(StateError) end end end From 28ff54b000abb4f5ef75d96008e0cfd76a660103 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Fri, 13 Feb 2015 03:16:48 -0500 Subject: [PATCH 010/195] VRFS-2695 wip new profile API --- db/manifest | 3 +- db/up/user_presences_rename.sql | 10 ++ ruby/lib/jam_ruby.rb | 2 +- .../{user_presence.rb => online_presence.rb} | 18 +- ruby/lib/jam_ruby/models/user.rb | 2 +- .../jam_ruby/models/online_presence_spec.rb | 160 ++++++++++++++++++ .../jam_ruby/models/user_presence_spec.rb | 160 ------------------ web/app/assets/javascripts/jam_rest.js | 14 ++ web/app/controllers/api_users_controller.rb | 11 ++ web/app/views/api_users/profile.rabl | 20 +++ web/app/views/api_users/show.rabl | 3 +- 11 files changed, 229 insertions(+), 174 deletions(-) create mode 100644 db/up/user_presences_rename.sql rename ruby/lib/jam_ruby/models/{user_presence.rb => online_presence.rb} (75%) create mode 100644 ruby/spec/jam_ruby/models/online_presence_spec.rb delete mode 100644 ruby/spec/jam_ruby/models/user_presence_spec.rb create mode 100644 web/app/views/api_users/profile.rabl diff --git a/db/manifest b/db/manifest index 6227fe0e1..d43f203a9 100755 --- a/db/manifest +++ b/db/manifest @@ -247,4 +247,5 @@ user_model_about_changes.sql performance_samples.sql user_presences.sql discard_scores_optimized.sql -alter_type_columns.sql \ No newline at end of file +alter_type_columns.sql +user_presences_rename.sql \ No newline at end of file diff --git a/db/up/user_presences_rename.sql b/db/up/user_presences_rename.sql new file mode 100644 index 000000000..92cb4e4ca --- /dev/null +++ b/db/up/user_presences_rename.sql @@ -0,0 +1,10 @@ +drop table user_presences; + +CREATE TABLE online_presences ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + service_type VARCHAR(100) NOT NULL, + username VARCHAR(100) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index b6ba623f6..4522b6c71 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -201,7 +201,7 @@ require "jam_ruby/models/recorded_video" require "jam_ruby/models/text_message" require "jam_ruby/jam_tracks_manager" require "jam_ruby/models/performance_sample" -require "jam_ruby/models/user_presence" +require "jam_ruby/models/online_presence" include Jampb diff --git a/ruby/lib/jam_ruby/models/user_presence.rb b/ruby/lib/jam_ruby/models/online_presence.rb similarity index 75% rename from ruby/lib/jam_ruby/models/user_presence.rb rename to ruby/lib/jam_ruby/models/online_presence.rb index 5f590e429..774ee0b94 100644 --- a/ruby/lib/jam_ruby/models/user_presence.rb +++ b/ruby/lib/jam_ruby/models/online_presence.rb @@ -1,5 +1,5 @@ module JamRuby - class UserPresence < ActiveRecord::Base + class OnlinePresence < ActiveRecord::Base PERMISSION_MSG = "You do not have permission to perform this operation." @@ -12,20 +12,20 @@ module JamRuby validate :username_service_type_unique def username_service_type_unique - match = UserPresence.exists?(:username => self.username, :service_type => self.service_type) + match = OnlinePresence.exists?(:username => self.username, :service_type => self.service_type) raise ConflictError, "Username #{self.username} is already associated with a #{self.service_type} account" if match end def self.index(options = {}) raise StateError, "The user is not specified." if options[:id].blank? - UserPresence.where("user_id = ?", options[:id]) + OnlinePresence.where("user_id = ?", options[:id]) end def self.create(current_user, options = {}) auth_user(current_user, options) raise StateError, "Missing required information" if options[:service_type].blank? || options[:username].blank? - up = UserPresence.new({:user_id => current_user.id, :service_type => options[:service_type], :username => options[:username]}) + up = OnlinePresence.new({:user_id => current_user.id, :service_type => options[:service_type], :username => options[:username]}) up.save! end @@ -33,7 +33,7 @@ module JamRuby auth_user(current_user, options) raise StateError, "Missing required information" if options[:service_type].blank? || options[:username].blank? || options[:id].blank? - up = UserPresence.find(options[:id]) + up = OnlinePresence.find(options[:id]) up.service_type = options[:service_type] up.username = options[:username] up.save! @@ -42,14 +42,14 @@ module JamRuby def self.delete(current_user, options = {}) id = options[:id] raise StateError, "Missing required information" if id.blank? - user_presence = UserPresence.find(id) + online_presence = OnlinePresence.find(id) - if user_presence.user_id != current_user.id + if online_presence.user_id != current_user.id raise PermissionError, PERMISSION_MSG end - unless user_presence.nil? - UserPresence.destroy(id) + unless online_presence.nil? + OnlinePresence.destroy(id) end end diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index a553e07d4..0927bf11d 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -152,7 +152,7 @@ module JamRuby # This causes the authenticate method to be generated (among other stuff) #has_secure_password - has_many :user_presences, :class_name => "JamRuby::UserPresence" + has_many :online_presences, :class_name => "JamRuby::OnlinePresence" has_many :performance_samples, :class_name => "JamRuby::PerformanceSample" before_save :create_remember_token, :if => :should_validate_password? diff --git a/ruby/spec/jam_ruby/models/online_presence_spec.rb b/ruby/spec/jam_ruby/models/online_presence_spec.rb new file mode 100644 index 000000000..ce642ac58 --- /dev/null +++ b/ruby/spec/jam_ruby/models/online_presence_spec.rb @@ -0,0 +1,160 @@ +require 'spec_helper' + +describe OnlinePresence do + + let(:user1) { FactoryGirl.create(:user) } + let(:user2) { FactoryGirl.create(:user) } + + describe "index" do + + before(:all) do + OnlinePresence.delete_all + + user1_presence1 = OnlinePresence.new({:user_id => user1.id, :username => "myonlineusername", :service_type => "facebook"}) + user1_presence1.save! + + user1_presence2 = OnlinePresence.new({:user_id => user1.id, :username => "myonlineusername", :service_type => "twitter"}) + user1_presence2.save! + + user2_presence1 = OnlinePresence.new({:user_id => user2.id, :username => "myonlineusername", :service_type => "soundcloud"}) + user2_presence1.save! + end + + context "when request is valid" do + it "should return all records for user" do + presence = OnlinePresence.index({:id => user1.id}) + presence.count.should == 2 + + presence = OnlinePresence.index({:id => user2.id}) + presence.count.should == 1 + end + end + + context "when request is invalid" do + it "should raise error when options are missing" do + lambda{OnlinePresence.index}.should raise_error(StateError) + end + + it "should raise error when user id is missing" do + lambda{OnlinePresence.index({:id => ""})}.should raise_error(StateError) + end + end + end + + describe "create" do + + before(:all) do + OnlinePresence.delete_all + end + + context "when request is valid" do + it "should save successfully" do + OnlinePresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1"}) + + # make sure we can save a second OnlinePresence for same user and type + OnlinePresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser2"}) + + OnlinePresence.index({:id => user1.id}).count.should == 2 + end + end + + context "when request is not valid" do + it "should raise PermissionError if requester id does not match id in request" do + lambda{OnlinePresence.create(user1, {:user_id => user2.id, :service_type => "soundcloud", :username => "soundclouduser2"})}.should raise_error(PermissionError) + end + + it "should raise error if service type is missing" do + lambda{OnlinePresence.create(user1, {:user_id => user1.id, :username => "soundclouduser2"})}.should raise_error(StateError) + end + + it "should raise error if username is missing" do + lambda{OnlinePresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud"})}.should raise_error(StateError) + end + + it "should not allow duplicates of the same username / service type combination" do + OnlinePresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1"}) + OnlinePresence.index({:id => user1.id}).count.should == 1 + + lambda{OnlinePresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1"})}.should raise_error(ConflictError) + OnlinePresence.index({:id => user1.id}).count.should == 1 + end + end + end + + describe "update" do + + before(:all) do + OnlinePresence.delete_all + @online_presence = OnlinePresence.new(:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1") + @online_presence.save! + end + + context "when request is valid" do + it "should save successfully" do + + up_list = OnlinePresence.index({:id => user1.id}) + up_list.count.should == 1 + up_list.first.service_type.should == "soundcloud" + up_list.first.username.should == "soundclouduser1" + + OnlinePresence.update(user1, {:id => @online_presence.id, :user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser2"}) + + up_list = OnlinePresence.index({:id => user1.id}) + up_list.count.should == 1 + up_list.first.service_type.should == "soundcloud" + up_list.first.username.should == "soundclouduser2" + end + end + + context "when request is not valid" do + it "should raise PermissionError if requester id does not match id in request" do + lambda{OnlinePresence.update(user1, {:user_id => user2.id, :id => @online_presence.id, :service_type => "soundcloud", :username => "soundclouduser2"})}.should raise_error(PermissionError) + end + + it "should raise error if type is missing" do + lambda{OnlinePresence.update(user1, {:user_id => user1.id, :id => @online_presence.id, :username => "soundclouduser2"})}.should raise_error(StateError) + end + + it "should raise error if username is missing" do + lambda{OnlinePresence.update(user1, {:user_id => user1.id, :id => @online_presence.id, :service_type => "soundcloud"})}.should raise_error(StateError) + end + + it "should raise error if user presence id is missing" do + lambda{OnlinePresence.update(user1, {:user_id => user1.id, :username => "soundclouduser2", :service_type => "soundcloud"})}.should raise_error(StateError) + end + end + end + + describe "destroy" do + + before(:all) do + OnlinePresence.delete_all + @online_presence = OnlinePresence.new(:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1") + @online_presence.save! + end + + context "when request is valid" do + it "should destroy successfully" do + up_list = OnlinePresence.index({:id => user1.id}) + up_list.count.should == 1 + up_list.first.service_type.should == "soundcloud" + up_list.first.username.should == "soundclouduser1" + + OnlinePresence.delete(user1, {:user_id => user1.id, :id => @online_presence.id}) + + up_list = OnlinePresence.index({:id => user1.id}) + up_list.count.should == 0 + end + end + + context "when request is not valid" do + it "should raise PermissionError if requester id does not match id in request" do + lambda{OnlinePresence.delete(user2, {:user_id => user1.id, :id => @online_presence.id})}.should raise_error(PermissionError) + end + + it "should raise error if user presence id is missing" do + lambda{OnlinePresence.delete(user1, {:user_id => user1.id})}.should raise_error(StateError) + end + end + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/user_presence_spec.rb b/ruby/spec/jam_ruby/models/user_presence_spec.rb deleted file mode 100644 index 06f51221a..000000000 --- a/ruby/spec/jam_ruby/models/user_presence_spec.rb +++ /dev/null @@ -1,160 +0,0 @@ -require 'spec_helper' - -describe UserPresence do - - let(:user1) { FactoryGirl.create(:user) } - let(:user2) { FactoryGirl.create(:user) } - - describe "index" do - - before(:all) do - UserPresence.delete_all - - user1_presence1 = UserPresence.new({:user_id => user1.id, :username => "myonlineusername", :service_type => "facebook"}) - user1_presence1.save! - - user1_presence2 = UserPresence.new({:user_id => user1.id, :username => "myonlineusername", :service_type => "twitter"}) - user1_presence2.save! - - user2_presence1 = UserPresence.new({:user_id => user2.id, :username => "myonlineusername", :service_type => "soundcloud"}) - user2_presence1.save! - end - - context "when request is valid" do - it "should return all records for user" do - presence = UserPresence.index({:id => user1.id}) - presence.count.should == 2 - - presence = UserPresence.index({:id => user2.id}) - presence.count.should == 1 - end - end - - context "when request is invalid" do - it "should raise error when options are missing" do - lambda{UserPresence.index}.should raise_error(StateError) - end - - it "should raise error when user id is missing" do - lambda{UserPresence.index({:id => ""})}.should raise_error(StateError) - end - end - end - - describe "create" do - - before(:all) do - UserPresence.delete_all - end - - context "when request is valid" do - it "should save successfully" do - UserPresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1"}) - - # make sure we can save a second UserPresence for same user and type - UserPresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser2"}) - - UserPresence.index({:id => user1.id}).count.should == 2 - end - end - - context "when request is not valid" do - it "should raise PermissionError if requester id does not match id in request" do - lambda{UserPresence.create(user1, {:user_id => user2.id, :service_type => "soundcloud", :username => "soundclouduser2"})}.should raise_error(PermissionError) - end - - it "should raise error if service type is missing" do - lambda{UserPresence.create(user1, {:user_id => user1.id, :username => "soundclouduser2"})}.should raise_error(StateError) - end - - it "should raise error if username is missing" do - lambda{UserPresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud"})}.should raise_error(StateError) - end - - it "should not allow duplicates of the same username / service type combination" do - UserPresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1"}) - UserPresence.index({:id => user1.id}).count.should == 1 - - lambda{UserPresence.create(user1, {:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1"})}.should raise_error(ConflictError) - UserPresence.index({:id => user1.id}).count.should == 1 - end - end - end - - describe "update" do - - before(:all) do - UserPresence.delete_all - @user_presence = UserPresence.new(:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1") - @user_presence.save! - end - - context "when request is valid" do - it "should save successfully" do - - up_list = UserPresence.index({:id => user1.id}) - up_list.count.should == 1 - up_list.first.service_type.should == "soundcloud" - up_list.first.username.should == "soundclouduser1" - - UserPresence.update(user1, {:id => @user_presence.id, :user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser2"}) - - up_list = UserPresence.index({:id => user1.id}) - up_list.count.should == 1 - up_list.first.service_type.should == "soundcloud" - up_list.first.username.should == "soundclouduser2" - end - end - - context "when request is not valid" do - it "should raise PermissionError if requester id does not match id in request" do - lambda{UserPresence.update(user1, {:user_id => user2.id, :id => @user_presence.id, :service_type => "soundcloud", :username => "soundclouduser2"})}.should raise_error(PermissionError) - end - - it "should raise error if type is missing" do - lambda{UserPresence.update(user1, {:user_id => user1.id, :id => @user_presence.id, :username => "soundclouduser2"})}.should raise_error(StateError) - end - - it "should raise error if username is missing" do - lambda{UserPresence.update(user1, {:user_id => user1.id, :id => @user_presence.id, :service_type => "soundcloud"})}.should raise_error(StateError) - end - - it "should raise error if user presence id is missing" do - lambda{UserPresence.update(user1, {:user_id => user1.id, :username => "soundclouduser2", :service_type => "soundcloud"})}.should raise_error(StateError) - end - end - end - - describe "destroy" do - - before(:all) do - UserPresence.delete_all - @user_presence = UserPresence.new(:user_id => user1.id, :service_type => "soundcloud", :username => "soundclouduser1") - @user_presence.save! - end - - context "when request is valid" do - it "should destroy successfully" do - up_list = UserPresence.index({:id => user1.id}) - up_list.count.should == 1 - up_list.first.service_type.should == "soundcloud" - up_list.first.username.should == "soundclouduser1" - - UserPresence.delete(user1, {:user_id => user1.id, :id => @user_presence.id}) - - up_list = UserPresence.index({:id => user1.id}) - up_list.count.should == 0 - end - end - - context "when request is not valid" do - it "should raise PermissionError if requester id does not match id in request" do - lambda{UserPresence.delete(user2, {:user_id => user1.id, :id => @user_presence.id})}.should raise_error(PermissionError) - end - - it "should raise error if user presence id is missing" do - lambda{UserPresence.delete(user1, {:user_id => user1.id})}.should raise_error(StateError) - end - end - end -end \ No newline at end of file diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 0a5d20c95..2cc7cfcc4 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -490,6 +490,20 @@ return detail; } + function getUserProfile(options) { + var id = getId(options); + var profile = null; + if (id != null && typeof(id) != 'undefined') { + profile = $.ajax({ + type: "GET", + dataType: "json", + url: "/api/users/" + id + "/profile", + processData: false + }); + } + return profile; + } + function getCities(options) { var country = options['country'] var region = options['region'] diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 34ea1154d..c4a986ad1 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -32,6 +32,17 @@ class ApiUsersController < ApiController respond_with @user, responder: ApiResponder, :status => 200 end + def profile + @profile = User.includes([{musician_instruments: :instrument}, + {band_musicians: :user}, + {genre_players: :genre}, + :bands, :instruments, :genres, + :user_presences, :performance_samples]) + .find(params[:id]) + + respond_with @profile, responder: ApiResponder, :status => 200 + end + def update @user = User.find(params[:id]) diff --git a/web/app/views/api_users/profile.rabl b/web/app/views/api_users/profile.rabl new file mode 100644 index 000000000..bdb137c20 --- /dev/null +++ b/web/app/views/api_users/profile.rabl @@ -0,0 +1,20 @@ +object @profile + +attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count, :age, :website, :skill_level, :concert_count, :studio_session_count, :virtual_band, :virtual_band_commitment, :traditional_band, :traditional_band_commitment, :traditional_band_touring, :paid_sessions, :paid_sessions_hourly_rate, +:paid_sessions_daily_rate, :free_sessions, :cowriting, :cowriting_purpose + +child :user_presences => :online_presences do + attributes :id, :service_type, :username +end + +child :performance_samples => :performance_samples do + attributes :id, :url, :service_type, :claimed_recording_id, :service_id + + child :claimed_recording => :claimed_recording do + attributes :name + end +end + +child :genre_players => :genres do + attributes :genre_id, :player_type +end \ No newline at end of file diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl index d402ff6a7..03e38f321 100644 --- a/web/app/views/api_users/show.rabl +++ b/web/app/views/api_users/show.rabl @@ -1,7 +1,6 @@ object @user -attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count, :age, :website, :skill_level, :concert_count, :studio_session_count, :virtual_band, :virtual_band_commitment, :traditional_band, :traditional_band_commitment, :traditional_band_touring, :paid_sessions, :paid_sessions_hourly_rate, -:paid_sessions_daily_rate, :free_sessions, :cowriting, :cowriting_purpose +attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count, :age, :website, :skill_level if @user.musician? node :location do @user.location end From a09894b9744f9e9f0971a4a5258960c9e04df9f9 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Fri, 13 Feb 2015 22:27:12 -0500 Subject: [PATCH 011/195] VRFS-2692 add genre_type to genre_players table --- db/manifest | 3 ++- db/up/add_genre_type.sql | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 db/up/add_genre_type.sql diff --git a/db/manifest b/db/manifest index d43f203a9..26d214706 100755 --- a/db/manifest +++ b/db/manifest @@ -248,4 +248,5 @@ performance_samples.sql user_presences.sql discard_scores_optimized.sql alter_type_columns.sql -user_presences_rename.sql \ No newline at end of file +user_presences_rename.sql +add_genre_type.sql \ No newline at end of file diff --git a/db/up/add_genre_type.sql b/db/up/add_genre_type.sql new file mode 100644 index 000000000..9c8312ada --- /dev/null +++ b/db/up/add_genre_type.sql @@ -0,0 +1 @@ +alter table genre_players add column genre_type varchar(20) default 'profile'; \ No newline at end of file From a778eaef282fbccf00acb3dabe122dd8a5bc580c Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Fri, 13 Feb 2015 22:45:19 -0500 Subject: [PATCH 012/195] VRFS-2695 wip get user profile API --- web/app/controllers/api_users_controller.rb | 7 +++++-- .../views/api_users/{profile.rabl => profile_show.rabl} | 4 ++-- web/config/routes.rb | 3 ++- 3 files changed, 9 insertions(+), 5 deletions(-) rename web/app/views/api_users/{profile.rabl => profile_show.rabl} (90%) diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index c4a986ad1..6266e2a25 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -32,17 +32,20 @@ class ApiUsersController < ApiController respond_with @user, responder: ApiResponder, :status => 200 end - def profile + def profile_show @profile = User.includes([{musician_instruments: :instrument}, {band_musicians: :user}, {genre_players: :genre}, :bands, :instruments, :genres, - :user_presences, :performance_samples]) + :online_presences, :performance_samples]) .find(params[:id]) respond_with @profile, responder: ApiResponder, :status => 200 end + def profile_save + end + def update @user = User.find(params[:id]) diff --git a/web/app/views/api_users/profile.rabl b/web/app/views/api_users/profile_show.rabl similarity index 90% rename from web/app/views/api_users/profile.rabl rename to web/app/views/api_users/profile_show.rabl index bdb137c20..820a3809b 100644 --- a/web/app/views/api_users/profile.rabl +++ b/web/app/views/api_users/profile_show.rabl @@ -3,7 +3,7 @@ object @profile attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count, :age, :website, :skill_level, :concert_count, :studio_session_count, :virtual_band, :virtual_band_commitment, :traditional_band, :traditional_band_commitment, :traditional_band_touring, :paid_sessions, :paid_sessions_hourly_rate, :paid_sessions_daily_rate, :free_sessions, :cowriting, :cowriting_purpose -child :user_presences => :online_presences do +child :online_presences => :online_presences do attributes :id, :service_type, :username end @@ -16,5 +16,5 @@ child :performance_samples => :performance_samples do end child :genre_players => :genres do - attributes :genre_id, :player_type + attributes :genre_id, :player_type, :genre_type end \ No newline at end of file diff --git a/web/config/routes.rb b/web/config/routes.rb index 79b53b120..06f96a41f 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -341,7 +341,8 @@ SampleApp::Application.routes.draw do match '/users/:id/share/recording/:provider' => 'api_users#share_recording', :via => :get #profile - match '/users/:id/profile' => 'api_users#profile', :via => :get, :as => 'api_users_profile' + match '/users/:id/profile' => 'api_users#profile_show', :via => :get, :as => 'api_users_profile_show' + match '/users/:id/profile' => 'api_users#profile_save', :via => :post # session chat match '/chat' => 'api_chats#create', :via => :post From 0a1837c4e62954bca62d03fdcf158265e07f9880 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 14 Feb 2015 21:02:26 -0500 Subject: [PATCH 013/195] VRFS-2695 VRFS-2696 wip get/update musician profile API changes --- ruby/lib/jam_ruby/constants/limits.rb | 2 +- ruby/lib/jam_ruby/models/genre_player.rb | 7 ++++ ruby/lib/jam_ruby/models/user.rb | 23 +++++++++--- web/app/controllers/api_users_controller.rb | 40 ++++++++++++++++++++- 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/ruby/lib/jam_ruby/constants/limits.rb b/ruby/lib/jam_ruby/constants/limits.rb index bbbe44fd7..ab4403b76 100644 --- a/ruby/lib/jam_ruby/constants/limits.rb +++ b/ruby/lib/jam_ruby/constants/limits.rb @@ -14,7 +14,7 @@ module Limits # instruments MIN_INSTRUMENTS_PER_MUSICIAN = 1 - MAX_INSTRUMENTS_PER_MUSICIAN = 5 + MAX_INSTRUMENTS_PER_MUSICIAN = 1000 # users USERS_CAN_INVITE = true diff --git a/ruby/lib/jam_ruby/models/genre_player.rb b/ruby/lib/jam_ruby/models/genre_player.rb index 76bdb4ccf..14848b269 100644 --- a/ruby/lib/jam_ruby/models/genre_player.rb +++ b/ruby/lib/jam_ruby/models/genre_player.rb @@ -1,6 +1,13 @@ module JamRuby class GenrePlayer < ActiveRecord::Base + PROFILE = 'profile' + VIRTUAL_BAND = 'virtual_band' + TRADITIONAL_BAND = 'traditional_band' + PAID_SESSION = 'paid_session' + FREE_SESSION = 'free_session' + COWRITING = 'cowriting' + self.table_name = "genre_players" belongs_to :player, polymorphic: true diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 0927bf11d..8bb7dd94a 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -631,16 +631,29 @@ module JamRuby return recordings end - def update_genres(gids) + def update_genres(gids, genre_type) unless self.new_record? - GenrePlayer.delete_all(["player_id = ? AND player_type = ?", - self.id, self.class.name]) + GenrePlayer.delete_all(["player_id = ? AND player_type = ? AND genre_type = ?", + self.id, self.class.name, genre_type]) end + gids.each do |gid| - self.genres << Genre.find_by_id(gid) + + genre_player = GenrePlayer.new + genre_player.player_id = self.id + genre_player.player_type = self.class.name + genre_player.genre_id = gid + genre_player.genre_type = genre_type + self.genre_players << genre_player end end + def update_online_presences(online_presences) + end + + def update_performance_samples(performance_samples) + end + # given an array of instruments, update a user's instruments def update_instruments(instruments) # delete all instruments for this user first @@ -661,6 +674,8 @@ module JamRuby end end + def + # this easy_save routine guards against nil sets, but many of these fields can be set to null. # I've started to use it less as I go forward def easy_save(first_name, last_name, email, password, password_confirmation, musician, gender, diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 6266e2a25..64029e612 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -58,10 +58,45 @@ class ApiUsersController < ApiController @user.country = params[:country] if params.has_key?(:country) @user.musician = params[:musician] if params.has_key?(:musician) @user.update_instruments(params[:instruments].nil? ? [] : params[:instruments]) if params.has_key?(:instruments) - @user.update_genres(params[:genres].nil? ? [] : params[:genres]) if params.has_key?(:genres) + + # genres + @user.update_genres(params[:genres].nil? ? [] : params[:genres], GenrePlayer::PROFILE) if params.has_key?(:genres) + @user.update_genres(params[:virtual_band_genres].nil? ? [] : params[:virtual_band_genres], GenrePlayer::VIRTUAL_BAND) if params.has_key?(:virtual_band_genres) + @user.update_genres(params[:traditional_band_genres].nil? ? [] : params[:traditional_band_genres], GenrePlayer::TRADITIONAL_BAND) if params.has_key?(:traditional_band_genres) + @user.update_genres(params[:paid_session_genres].nil? ? [] : params[:paid_session_genres], GenrePlayer::PAID_SESSION) if params.has_key?(:paid_session_genres) + @user.update_genres(params[:free_session_genres].nil? ? [] : params[:free_session_genres], GenrePlayer::FREE_SESSION) if params.has_key?(:free_session_genres) + @user.update_genres(params[:cowriting_genres].nil? ? [] : params[:cowriting_genres], GenrePlayer::COWRITING) if params.has_key?(:cowriting_genres) + @user.show_whats_next = params[:show_whats_next] if params.has_key?(:show_whats_next) @user.subscribe_email = params[:subscribe_email] if params.has_key?(:subscribe_email) @user.biography = params[:biography] if params.has_key?(:biography) + + @user.website = params[:website] if params.has_key?(:website) + @user.skill_level = params[:skill_level] if params.has_key?(:skill_level) + @user.concert_count = params[:concert_count] if params.has_key?(:concert_count) + @user.studio_session_count = params[:studio_session_count] if params.has_key?(:studio_session_count) + + # virtual band + @user.virtual_band = params[:virtual_band] if params.has_key?(:virtual_band) + @user.virtual_band_commitment = params[:virtual_band_commitment] if params.has_key?(:virtual_band_commitment) + + # traditional band + @user.traditional_band = params[:traditional_band] if params.has_key?(:traditional_band) + @user.traditional_band_commitment = params[:traditional_band_commitment] if params.has_key?(:traditional_band_commitment) + @user.traditional_band_touring = params[:traditional_band_touring] if params.has_key?(:traditional_band_touring) + + # paid sessions + @user.paid_sessions = params[:paid_sessions] if params.has_key?(:paid_sessions) + @user.paid_sessions_hourly_rate = params[:paid_sessions_hourly_rate] if params.has_key?(:paid_sessions_hourly_rate) + @user.paid_sessions_daily_rate = params[:paid_sessions_daily_rate] if params.has_key?(:paid_sessions_daily_rate) + + # free sessions + @user.free_sessions = params[:free_sessions] if params.has_key?(:free_sessions) + + # co-writing + @user.cowriting = params[:cowriting] if params.has_key?(:cowriting) + @user.cowriting_purpose = params[:cowriting_purpose] if params.has_key?(:cowriting_purpose) + @user.mod_merge(params[:mods]) if params[:mods] # allow keyword of 'LATEST' to mean set the notification_seen_at to the most recent notification for this user @@ -69,6 +104,9 @@ class ApiUsersController < ApiController @user.update_notification_seen_at params[:notification_seen_at] end + @user.update_online_presences(params[:online_presences]) if params.has_key?(:online_presences) + @user.update_performance_samples(params[:performance_samples]) if params.has_key?(:performance_samples) + @user.save if @user.errors.any? From 4b97807fc87e8b569c5e26dfc6e2e051310de171 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 14 Feb 2015 21:14:13 -0500 Subject: [PATCH 014/195] fix syntax error --- ruby/lib/jam_ruby/models/user.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 8bb7dd94a..32a5a1a2d 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -674,8 +674,6 @@ module JamRuby end end - def - # this easy_save routine guards against nil sets, but many of these fields can be set to null. # I've started to use it less as I go forward def easy_save(first_name, last_name, email, password, password_confirmation, musician, gender, From 09b4ed09a4e5a04ad78b1c46c9cee635cab8246c Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sun, 15 Feb 2015 08:23:26 -0500 Subject: [PATCH 015/195] VRFS-2696 edit musician profile API enhancements --- ruby/lib/jam_ruby/models/online_presence.rb | 12 +++++++++--- ruby/lib/jam_ruby/models/performance_sample.rb | 5 +++-- ruby/lib/jam_ruby/models/user.rb | 17 ++++++++++++++++- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/ruby/lib/jam_ruby/models/online_presence.rb b/ruby/lib/jam_ruby/models/online_presence.rb index 774ee0b94..8061ac571 100644 --- a/ruby/lib/jam_ruby/models/online_presence.rb +++ b/ruby/lib/jam_ruby/models/online_presence.rb @@ -21,12 +21,18 @@ module JamRuby OnlinePresence.where("user_id = ?", options[:id]) end - def self.create(current_user, options = {}) + def self.create(current_user, options = {}, save = true) auth_user(current_user, options) raise StateError, "Missing required information" if options[:service_type].blank? || options[:username].blank? - up = OnlinePresence.new({:user_id => current_user.id, :service_type => options[:service_type], :username => options[:username]}) - up.save! + up = OnlinePresence.new({ + :user_id => current_user.id, + :service_type => options[:service_type], + :username => options[:username] + }) + + up.save! if save + up end def self.update(current_user, options = {}) diff --git a/ruby/lib/jam_ruby/models/performance_sample.rb b/ruby/lib/jam_ruby/models/performance_sample.rb index df405275d..db02b26c1 100644 --- a/ruby/lib/jam_ruby/models/performance_sample.rb +++ b/ruby/lib/jam_ruby/models/performance_sample.rb @@ -41,7 +41,7 @@ module JamRuby PerformanceSample.where("user_id = ?", options[:id]) end - def self.create(current_user, options = {}) + def self.create(current_user, options = {}, save = true) auth_user(current_user, options) raise StateError, "Missing required information" if options[:service_type].blank? @@ -53,7 +53,8 @@ module JamRuby :url => options[:url] }) - ps.save! + ps.save! if save + ps end def self.delete(current_user, options = {}) diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 32a5a1a2d..d7c5dd90c 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -638,7 +638,6 @@ module JamRuby end gids.each do |gid| - genre_player = GenrePlayer.new genre_player.player_id = self.id genre_player.player_type = self.class.name @@ -649,9 +648,25 @@ module JamRuby end def update_online_presences(online_presences) + unless self.new_record? + OnlinePresence.delete_all(["user_id = ?", self.id]) + end + + online_presences.each do |op| + op = OnlinePresence.create(self.id, online_presences, false) + self.online_presences << op + end end def update_performance_samples(performance_samples) + unless self.new_record? + PerformanceSample.delete_all(["user_id = ?", self.id]) + end + + performance_samples.each do |ps| + ps = PerformanceSample.create(self.id, performance_samples, false) + self.performance_samples << ps + end end # given an array of instruments, update a user's instruments From 20baeb8bd3a6dd860d148ccc1c5022ce9c9f09d6 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 21 Feb 2015 09:20:11 -0500 Subject: [PATCH 016/195] VRFS-2701 wip UI changes for musician profile enhancements --- ruby/lib/jam_ruby/models/user.rb | 2 +- web/app/assets/javascripts/profile.js | 29 ++++++------------- .../stylesheets/client/profile.css.scss | 10 +++++-- .../views/clients/_account_profile.html.erb | 2 +- web/app/views/clients/_profile.html.erb | 23 ++++++++------- 5 files changed, 32 insertions(+), 34 deletions(-) diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index fe912bd64..08091e3e9 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -348,7 +348,7 @@ module JamRuby def age now = Time.now.utc.to_date - self.birth_date.nil? ? "unspecified" : now.year - self.birth_date.year - (self.birth_date.to_date.change(:year => now.year) > now ? 1 : 0) + self.birth_date.nil? ? "" : now.year - self.birth_date.year - (self.birth_date.to_date.change(:year => now.year) > now ? 1 : 0) end def session_count diff --git a/web/app/assets/javascripts/profile.js b/web/app/assets/javascripts/profile.js index 49543492d..cd5be333c 100644 --- a/web/app/assets/javascripts/profile.js +++ b/web/app/assets/javascripts/profile.js @@ -362,6 +362,8 @@ // location $('#profile-location').html(user.location); + $('#profile-age').html(user.age ? user.age + " years old" : ""); + // stats var text = user.friend_count > 1 || user.friend_count === 0 ? " Friends" : " Friend"; $('#profile-friend-stats').html('' + user.friend_count + '' + text); @@ -369,6 +371,9 @@ text = user.follower_count > 1 || user.follower_count === 0 ? " Followers" : " Follower"; $('#profile-follower-stats').html('' + user.follower_count + '' + text); + // text = user.following_count > 1 || user.following_count === 0 ? " Followings" : " Following"; + // $('#profile-following-stats').html('' + user.following_count + '' + text); + if (isMusician()) { text = user.session_count > 1 || user.session_count === 0 ? " Sessions" : " Session"; $('#profile-session-stats').html(user.session_count + text); @@ -400,12 +405,6 @@ if(user.biography) { $showBio.show(); - if(isCurrentUser()) { - $editBiographyButton.show(); - } - else { - $editBiographyButton.hide(); - } $biographyText.text(user.biography).show(); } else { @@ -419,26 +418,16 @@ var $showBio = $('.have-bio', profileScreen); var $noBio = $('.no-bio', profileScreen); var $biographyEditor = $('.update-biography', profileScreen); - var $addBiographyButton = $('a.enter-bio', profileScreen); - var $editBiographyButton = $('#profile-edit-biography', profileScreen); var $submitBiographyButton = $('#btn-update-user-biography', profileScreen); var $cancelBiographyButton = $('#btn-cancel-user-biography', profileScreen); var $biographyText = $('#profile-biography', profileScreen); initializeBioVisibility(); - $addBiographyButton.unbind('click').click(function() { - $biographyEditor.val(user.biography).show(); - return false; - }); - - $editBiographyButton.unbind('click').click(function() { - $editBiographyButton.hide(); - $biographyText.hide(); - $bioTextArea.val(user.biography); - $biographyEditor.show(); - return false; - }) + // $addBiographyButton.unbind('click').click(function() { + // $biographyEditor.val(user.biography).show(); + // return false; + // }); $submitBiographyButton.unbind('click').click(function() { var bio = $bioTextArea.val(); diff --git a/web/app/assets/stylesheets/client/profile.css.scss b/web/app/assets/stylesheets/client/profile.css.scss index b19b16f9e..c260b7e26 100644 --- a/web/app/assets/stylesheets/client/profile.css.scss +++ b/web/app/assets/stylesheets/client/profile.css.scss @@ -19,14 +19,20 @@ } .profile-header { padding:10px 20px; -// height:120px; } .profile-header h2 { font-weight:200; font-size: 28px; float:left; - margin: 0px 150px 0px 0px; + margin: 0px 0px 0px 0px; +} + +.profile-about-right .section { + font-weight:bold; + font-size:20px; + float:left; + margin: 0px 0px 0px 0px; } .profile-status { diff --git a/web/app/views/clients/_account_profile.html.erb b/web/app/views/clients/_account_profile.html.erb index af76ee2e7..d95a6d5b8 100644 --- a/web/app/views/clients/_account_profile.html.erb +++ b/web/app/views/clients/_account_profile.html.erb @@ -27,7 +27,7 @@
-

profile:

+

edit profile: basics

diff --git a/web/app/views/clients/_profile.html.erb b/web/app/views/clients/_profile.html.erb index 2316b7730..64797c7e1 100644 --- a/web/app/views/clients/_profile.html.erb +++ b/web/app/views/clients/_profile.html.erb @@ -13,8 +13,10 @@
- -

+
+

+ <%= link_to("EDIT PROFILE", '/client#/account/profile', :id => "btn-profile-edit", :class => "button-orange") %> +
@@ -25,7 +27,6 @@ ADD FRIEND FOLLOW MESSAGE - <%= link_to("EDIT PROFILE", '/client#/account/profile', :id => "btn-profile-edit", :class => "button-orange") %>


@@ -52,23 +53,25 @@

Location:


-


+
+

Stats:




-
-


+
+
+
Bio
- You have no bio to describe yourself as a musician. Enter one now! + Not specified. Edit Bio
-

EDIT BIO +

-
+

From 3e473a9ec397f2b6d425d6c793a851a913ae1418 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 21 Feb 2015 23:26:08 -0500 Subject: [PATCH 017/195] VRFS-2701 wip for musician profile UI enhancements --- web/app/assets/javascripts/jam_rest.js | 1 + web/app/assets/javascripts/profile.js | 362 +++++++++++------- web/app/assets/javascripts/profile_utils.js | 69 ++++ .../stylesheets/client/profile.css.scss | 18 +- web/app/views/api_users/profile_show.rabl | 19 +- web/app/views/clients/_profile.html.erb | 329 +++++++++------- 6 files changed, 503 insertions(+), 295 deletions(-) create mode 100644 web/app/assets/javascripts/profile_utils.js diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index a89143c97..c89414c64 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -1600,6 +1600,7 @@ this.cancelSession = cancelSession; this.updateScheduledSession = updateScheduledSession; this.getUserDetail = getUserDetail; + this.getUserProfile = getUserProfile; this.getCities = getCities; this.getRegions = getRegions; this.getCountries = getCountries; diff --git a/web/app/assets/javascripts/profile.js b/web/app/assets/javascripts/profile.js index cd5be333c..0eebdfe10 100644 --- a/web/app/assets/javascripts/profile.js +++ b/web/app/assets/javascripts/profile.js @@ -11,9 +11,65 @@ var rest = context.JK.Rest(); var decrementedFriendCountOnce = false; var sentFriendRequest = false; - var profileScreen = null; var textMessageDialog = null; var feed = null; + var profileUtils = context.JK.ProfileUtils; + + var NOT_SPECIFIED_TEXT = 'Not specified'; + + var $screen = $('#user-profile'); + var $instruments = $screen.find('#instruments'); + var $musicianStatus = $screen.find('#musician-status'); + var $genres = $screen.find('#genres'); + var $concertCount = $screen.find('#concert-count'); + var $studioCount = $screen.find('#studio-count'); + + var $aboutLink = $screen.find('#about-link'); + var $aboutContent = $screen.find('#about-content'); + + var $historyLink = $screen.find('#history-link'); + var $historyContent = $screen.find('#history-content'); + + var $bandsLink = $screen.find('#bands-link'); + var $bandsContent = $screen.find('#bands-content'); + + var $socialLink = $screen.find('#social-link'); + var $socialContent = $screen.find('#social-content'); + + var $favoritesLink = $screen.find('#favorites-link'); + var $favoritesContent = $screen.find('#favorites-content'); + + var $friendStats = $screen.find('#friend-stats'); + var $followerStats = $screen.find('#follower-stats'); + var $sessionStats = $screen.find('#session-stats'); + var $recordingStats = $screen.find('#recording-stats'); + var $followingStats = $screen.find('#following-stats'); + var $favoriteStats = $screen.find('#favorite-stats'); + + var $typeLabel = $screen.find('#type-label'); + var $location = $screen.find('#location'); + var $age = $screen.find('#age'); + + var $btnEdit = $screen.find('#btn-edit'); + var $btnAddFriend = $screen.find('#btn-add-friend'); + var $btnFollowUser = $screen.find('#btn-follow-user'); + var $btnMessageUser = $screen.find('#btn-message-user'); + + var $socialLeft = $screen.find('.profile-social-left'); + var $socialFriends = $screen.find('#social-friends'); + var $socialFollowings = $screen.find('#social-followings'); + var $socialFollowers = $screen.find('#social-followers'); + + var $userName = $screen.find('#username'); + var $avatar = $screen.find('#avatar'); + + var $bioTextArea = $screen.find('.user-biography'); + var $showBio = $screen.find('.have-bio'); + var $noBio = $screen.find('.no-bio'); + var $biographyEditor = $screen.find('.update-biography'); + var $submitBiographyButton = $screen.find('#btn-update-user-biography'); + var $cancelBiographyButton = $screen.find('#btn-cancel-user-biography'); + var $biographyText = $screen.find('#biography'); var instrument_logo_map = context.JK.getInstrumentIconMap24(); @@ -23,6 +79,27 @@ "3": "EXPERT" }; + var skillLevelMap = { + "1": "Amateur", + "2": "Professional" + } + + var concertGigMap = { + "0": "zero", + "1": "under 10", + "2": "10 to 50", + "3": "50 to 100", + "4": "over 100" + } + + var studioGigMap = { + "0": "zero", + "1": "under 10", + "2": "10 to 50", + "3": "50 to 100", + "4": "over 100" + } + var proficiencyCssMap = { "1": "proficiency-beginner", "2": "proficiency-intermediate", @@ -44,23 +121,23 @@ } function resetForm() { - $('#profile-instruments').empty(); + $instruments.empty(); - $('#profile-about').show(); - $('#profile-history').hide(); - $('#profile-bands').hide(); - $('#profile-social').hide(); - $('#profile-favorites').hide(); + $aboutContent.show(); + $historyContent.hide(); + $bandsContent.hide(); + $socialContent.hide(); + $favoritesContent.hide(); $('.profile-nav a.active').removeClass('active'); - $('.profile-nav a#profile-about-link').addClass('active'); + $aboutLink.addClass('active'); } function initUser() { user = null; decrementedFriendCountOnce = false; sentFriendRequest = false; - userDefer = rest.getUserDetail({id: userId}) + userDefer = rest.getUserProfile({id: userId}) .done(function (response) { user = response; configureUserType(); @@ -89,43 +166,43 @@ function configureUserType() { if (isMusician()) { - $('#profile-history-link').show(); - $('#profile-bands-link').show(); - $('#profile-instruments').show(); - $('#profile-session-stats').show(); - $('#profile-recording-stats').show(); - // $('#profile-following-stats').hide(); - // $('#profile-favorites-stats').hide(); - $('.profile-social-left').show(); - $('#profile-type-label').text('musician'); - $('#profile-location-label').text('Location'); + $historyLink.show(); + $bandsLink.show(); + $instruments.show(); + $sessionStats.show(); + $recordingStats.show(); + // $followingStats.hide(); + // $favoriteStats.hide(); + $socialLeft.show(); + $typeLabel.text('musician'); + $location.text('Location'); } else { - $('#profile-history-link').hide(); - $('#profile-bands-link').hide(); - $('#profile-instruments').hide(); - $('#profile-session-stats').hide(); - $('#profile-recording-stats').hide(); - // $('#profile-following-stats').show(); - // $('#profile-favorites-stats').show(); - $('.profile-social-left').hide(); - $('#profile-type-label').text('fan'); - $('#profile-location-label').text('Presence'); + $historyLink.hide(); + $bandsLink.hide(); + $instruments.hide(); + $sessionStats.hide(); + $recordingStats.hide(); + // $followingStats.show(); + // $favoriteStats.show(); + $socialLeft.hide(); + $typeLabel.text('fan'); + $location.text('Presence'); } if (isCurrentUser()) { - $('#btn-profile-edit').show(); - $('#btn-add-friend').hide(); - $('#btn-follow-user').hide(); - $('#btn-message-user').hide(); + $btnEdit.show(); + $btnAddFriend.hide(); + $btnFollowUser.hide(); + $btnMessageUser.hide(); } else { configureFriendFollowersControls(); - $('#btn-profile-edit').hide(); - $('#btn-add-friend').show(); - $('#btn-follow-user').show(); - $('#btn-message-user').show(); + $btnEdit.hide(); + $btnAddFriend.show(); + $btnFollowUser.show(); + $btnMessageUser.show(); } } @@ -141,26 +218,30 @@ // events for main screen function events() { // wire up panel clicks -- these need to check deferred because they can't be hidden when in an invalid state - $('#profile-about-link').click(function () { + $aboutLink.click(function () { renderTabDeferred(renderAbout) }); - $('#profile-history-link').click(function () { + + $historyLink.click(function () { renderTabDeferred(renderHistory) }); - $('#profile-bands-link').click(function () { + + $bandsLink.click(function () { renderTabDeferred(renderBands) }); - $('#profile-social-link').click(function () { + + $socialLink.click(function () { renderTabDeferred(renderSocial) }); - $('#profile-favorites-link').click(function () { + + $favoritesLink.click(function () { renderTabDeferred(renderFavorites) }); // this doesn't need deferred because it's only shown when valid - $('#btn-add-friend').click(handleFriendChange); - $('#btn-follow-user').click(handleFollowingChange); - $('#btn-message-user').click(handleMessageMusician); + $btnAddFriend.click(handleFriendChange); + $btnFollowUser.click(handleFollowingChange); + $btnMessageUser.click(handleMessageMusician); } function handleFriendChange(evt) { @@ -221,10 +302,10 @@ function configureFriendButton() { if (isFriend()) { - $('#btn-add-friend').text('DISCONNECT'); + $btnAddFriend.text('DISCONNECT'); } else { - $('#btn-add-friend').text('CONNECT'); + $btnAddFriend.text('CONNECT'); } } @@ -267,32 +348,32 @@ function configureFollowingButton() { if (isFollowing()) { - $('#btn-follow-user').text('UNFOLLOW'); + $btnFollowUser.text('UNFOLLOW'); } else { - $('#btn-follow-user').text('FOLLOW'); + $btnFollowUser.text('FOLLOW'); } } function configureEditProfileButton() { - $('#btn-follow-user').click(addFollowing); + $btnFollowUser.click(addFollowing); } // refreshes the currently active tab function renderActive() { - if ($('#profile-about-link').hasClass('active')) { + if ($aboutLink.hasClass('active')) { renderAbout(); } - else if ($('#profile-history-link').hasClass('active')) { + else if ($historyLink.hasClass('active')) { renderHistory(); } - else if ($('#profile-bands-link').hasClass('active')) { + else if ($bandsLink.hasClass('active')) { renderBands(); } - else if ($('#profile-social-link').hasClass('active')) { + else if ($socialLink.hasClass('active')) { renderSocial(); } - else if ($('#profile-favorites-link').hasClass('active')) { + else if ($favoritesLink.hasClass('active')) { renderFavorites(); } } @@ -310,29 +391,29 @@ /****************** ABOUT TAB *****************/ function renderAbout() { - $('#profile-instruments').empty(); + $instruments.empty(); - $('#profile-about').show(); - $('#profile-history').hide(); - $('#profile-bands').hide(); - $('#profile-social').hide(); - $('#profile-favorites').hide(); + $aboutContent.show(); + $historyContent.hide(); + $bandsContent.hide(); + $socialContent.hide(); + $favoritesContent.hide(); $('.profile-nav a.active').removeClass('active'); - $('.profile-nav a#profile-about-link').addClass('active'); + $aboutLink.addClass('active'); bindAbout(); } function bindAbout() { - $('#profile-instruments').empty(); + $instruments.empty(); // name - $('#profile-username').html(user.name); + $userName.html(user.name); // avatar - $('#profile-avatar').attr('src', context.JK.resolveAvatarUrl(user.photo_url)); + $avatar.attr('src', context.JK.resolveAvatarUrl(user.photo_url)); // instruments if (user.instruments) { @@ -351,40 +432,58 @@ proficiency_level_css: proficiencyCssMap[proficiency] }); - $('#profile-instruments').append(instrumentHtml); + $instruments.append(instrumentHtml); } } - $('#profile-genres').empty(); - for (var i=0; i< user.genres.length; i++) { - $('#profile-genres').append(user.genres[i].description + '
'); + + // status + var status = user.skill_level; + $musicianStatus.html(status ? skillLevelMap[status] + ' musician' : NOT_SPECIFIED_TEXT) + + // genres + $genres.empty(); + var profileGenres = profileUtils.profileGenres(user.genres); + for (var i=0; i < profileGenres.length; i++) { + $genres.append(profileGenres[i].genre_id); + if (i !== profileGenres.length - 1) { + $genres.append(', '); + } } - // location - $('#profile-location').html(user.location); + // concert gigs + var concertGigCount = user.concert_count; + $concertCount.html(concertGigCount > 0 ? 'Has played ' + concertGigCount + ' live concert gigs' : NOT_SPECIFIED_TEXT); - $('#profile-age').html(user.age ? user.age + " years old" : ""); + // studio gigs + var studioGigCount = user.studio_session_count; + $studioCount.html(studioGigCount > 0 ? 'Has played ' + studioGigCount + ' studio session gigs' : NOT_SPECIFIED_TEXT); + + // location + $location.html(user.location); + + $age.html(user.age ? user.age + " years old" : ""); // stats var text = user.friend_count > 1 || user.friend_count === 0 ? " Friends" : " Friend"; - $('#profile-friend-stats').html('' + user.friend_count + '' + text); + $friendStats.html('' + user.friend_count + '' + text); text = user.follower_count > 1 || user.follower_count === 0 ? " Followers" : " Follower"; - $('#profile-follower-stats').html('' + user.follower_count + '' + text); + $followerStats.html('' + user.follower_count + '' + text); // text = user.following_count > 1 || user.following_count === 0 ? " Followings" : " Following"; // $('#profile-following-stats').html('' + user.following_count + '' + text); if (isMusician()) { text = user.session_count > 1 || user.session_count === 0 ? " Sessions" : " Session"; - $('#profile-session-stats').html(user.session_count + text); + $sessionStats.html(user.session_count + text); text = user.recording_count > 1 || user.recording_count === 0 ? " Recordings" : " Recording"; - $('#profile-recording-stats').html(user.recording_count + text); + $recordingStats.html(user.recording_count + text); } else { text = " Following"; - $('#profile-following-stats').html(user.following_count + text); + $followingStats.html(user.following_count + text); text = user.favorite_count > 1 || user.favorite_count === 0 ? " Favorites" : " Favorite"; - $('#profile-favorite-stats').html(user.favorite_count + text); + $favoriteStats.html(user.favorite_count + text); } renderBio(); @@ -414,14 +513,6 @@ } } - var $bioTextArea = $('.user-biography', profileScreen); - var $showBio = $('.have-bio', profileScreen); - var $noBio = $('.no-bio', profileScreen); - var $biographyEditor = $('.update-biography', profileScreen); - var $submitBiographyButton = $('#btn-update-user-biography', profileScreen); - var $cancelBiographyButton = $('#btn-cancel-user-biography', profileScreen); - var $biographyText = $('#profile-biography', profileScreen); - initializeBioVisibility(); // $addBiographyButton.unbind('click').click(function() { @@ -466,18 +557,18 @@ /****************** SOCIAL TAB *****************/ function renderSocial() { - $('#profile-social-friends').empty(); - $('#profile-social-followings').empty(); - $('#profile-social-followers').empty(); + $socialFriends.empty(); + $socialFollowings.empty(); + $socialFollowers.empty(); - $('#profile-about').hide(); - $('#profile-history').hide(); - $('#profile-bands').hide(); - $('#profile-social').show(); - $('#profile-favorites').hide(); + $aboutContent.hide(); + $historyContent.hide(); + $bandsContent.hide(); + $socialContent.show(); + $favoritesContent.hide(); $('.profile-nav a.active').removeClass('active'); - $('.profile-nav a#profile-social-link').addClass('active'); + $socialLink.addClass('active'); bindSocial(); } @@ -500,11 +591,11 @@ type: "Friends" }); - $('#profile-social-friends').append(friendHtml); + $socialFriends.append(friendHtml); }); } else { - $('#profile-social-friends').html(' '); + $socialFriends.html(' '); } context.JK.bindHoverEvents(); }) @@ -525,11 +616,11 @@ location: val.location }); - $('#profile-social-followings').append(followingHtml); + $socialFollowings.append(followingHtml); }); } else { - $('#profile-social-followings').html(' '); + $socialFollowings.html(' '); } context.JK.bindHoverEvents(); }) @@ -548,7 +639,7 @@ location: val.location }); - $('#profile-social-followers').append(followerHtml); + $socialFollowers.append(followerHtml); }); context.JK.bindHoverEvents(); }) @@ -557,14 +648,14 @@ /****************** HISTORY TAB *****************/ function renderHistory() { - $('#profile-about').hide(); - $('#profile-history').show(); - $('#profile-bands').hide(); - $('#profile-social').hide(); - $('#profile-favorites').hide(); + $aboutContent.hide(); + $historyContent.show(); + $bandsContent.hide(); + $socialContent.hide(); + $favoritesContent.hide(); $('.profile-nav a.active').removeClass('active'); - $('.profile-nav a#profile-history-link').addClass('active'); + $historyLink.addClass('active'); bindHistory(); } @@ -575,16 +666,16 @@ /****************** BANDS TAB *****************/ function renderBands() { - $('#profile-bands').empty(); + $bandsContent.empty(); - $('#profile-about').hide(); - $('#profile-history').hide(); - $('#profile-bands').show(); - $('#profile-social').hide(); - $('#profile-favorites').hide(); + $aboutContent.hide(); + $historyContent.hide(); + $bandsContent.show(); + $socialContent.hide(); + $favoritesContent.hide(); $('.profile-nav a.active').removeClass('active'); - $('.profile-nav a#profile-bands-link').addClass('active'); + $bandsLink.addClass('active'); bindBands(); } @@ -595,7 +686,7 @@ .done(function (response) { if ((!response || response.length === 0) && isCurrentUser()) { var noBandHtml = $('#template-no-bands').html(); - $("#profile-bands").html(noBandHtml); + $bandsContent.html(noBandHtml); } else { addMoreBandsLink(); @@ -653,7 +744,7 @@ musicians: musicianHtml }); - $('#profile-bands').append(bandHtml); + $bandsContent.append(bandHtml); $('.profile-band-link-member-true').each(function(idx) { isBandMember ? $(this).show() : $(this).hide(); @@ -678,7 +769,7 @@ function addMoreBandsLink() { if (isCurrentUser()) { var moreBandsHtml = $('#template-more-bands').html(); - $("#profile-bands").append(moreBandsHtml); + $bandsContent.append(moreBandsHtml); } } @@ -705,13 +796,13 @@ } function updateFollowingCount(value) { - var followingCount = $('#profile-follower-stats span.follower-count'); - followingCount.text(value + parseInt(followingCount.text())); + var $followingCount = $('#follower-stats span.follower-count'); + $followingCount.text(value + parseInt($followingCount.text())); } function updateBandFollowingCount(bandId, value) { - var bandFollowing = $('div[band-id="' + bandId + '"].profile-bands span.follower-count'); - bandFollowing.text(value + parseInt(bandFollowing.text())); + var $bandFollowing = $('div[band-id="' + bandId + '"].bands span.follower-count'); + $bandFollowing.text(value + parseInt($bandFollowing.text())); } function addBandFollowing(evt) { @@ -732,7 +823,7 @@ } function configureBandFollowingButton(following, bandId) { - var $btnFollowBand = $('div[band-id=' + bandId + ']', '#profile-bands').find('#btn-follow-band-2'); + var $btnFollowBand = $('div[band-id=' + bandId + ']', '#bands').find('#btn-follow-band'); $btnFollowBand.unbind("click"); if (following) { @@ -751,14 +842,14 @@ /****************** FAVORITES TAB *****************/ function renderFavorites() { - $('#profile-about').hide(); - $('#profile-history').hide(); - $('#profile-bands').hide(); - $('#profile-social').hide(); - $('#profile-favorites').show(); + $aboutContent.hide(); + $historyContent.hide(); + $bandsContent.hide(); + $socialContent.hide(); + $favoritesContent.show(); $('.profile-nav a.active').removeClass('active'); - $('.profile-nav a#profile-favorites-link').addClass('active'); + $favoritesLink.addClass('active'); bindFavorites(); } @@ -769,16 +860,16 @@ function initializeFeed() { - var $scroller = profileScreen.find('.content-body-scroller#user-profile-feed-scroller'); - var $content = profileScreen.find('.feed-content#user-profile-feed-entry-list'); - var $noMoreFeeds = $('#user-profile-end-of-feeds-list'); - var $refresh = profileScreen.find('.btn-refresh-entries'); - var $sortFeedBy = profileScreen.find('#feed_order_by'); - var $includeDate = profileScreen.find('#feed_date'); - var $includeType = profileScreen.find('#feed_show'); + var $scroller = $screen.find('.content-body-scroller#user-profile-feed-scroller'); + var $content = $screen.find('.feed-content#user-profile-feed-entry-list'); + var $noMoreFeeds = $screen.find('#user-profile-end-of-feeds-list'); + var $refresh = $screen.find('.btn-refresh-entries'); + var $sortFeedBy = $screen.find('#feed_order_by'); + var $includeDate = $screen.find('#feed_date'); + var $includeType = $screen.find('#feed_show'); feed = new context.JK.Feed(app); - feed.initialize(profileScreen, $scroller, $content, $noMoreFeeds, $refresh, $sortFeedBy, $includeDate, $includeType, {time_range: 'all'}); + feed.initialize($screen, $scroller, $content, $noMoreFeeds, $refresh, $sortFeedBy, $includeDate, $includeType, {time_range: 'all'}); } function initialize(textMessageDialogInstance) { @@ -789,7 +880,6 @@ 'beforeHide' : beforeHide }; app.bindScreen('profile', screenBindings); - profileScreen = $('#user-profile'); events(); initializeFeed(); } diff --git a/web/app/assets/javascripts/profile_utils.js b/web/app/assets/javascripts/profile_utils.js new file mode 100644 index 000000000..d01fbe2fc --- /dev/null +++ b/web/app/assets/javascripts/profile_utils.js @@ -0,0 +1,69 @@ +/** + * Common utility functions. + */ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + var profileUtils = {}; + context.JK.ProfileUtils = profileUtils; + + var PROFILE_GENRE_TYPE = 'profile'; + var VIRTUAL_BAND_GENRE_TYPE = 'virtual_band'; + var TRADITIONAL_BAND_GENRE_TYPE = 'traditional_band'; + var PAID_SESSION_GENRE_TYPE = 'paid_session'; + var FREE_SESSION_GENRE_TYPE = 'free_session'; + var COWRITING_GENRE_TYPE = 'cowriting'; + + var USER_TYPE = 'JamRuby::User'; + + profileUtils.profileGenres = function(genres) { + var matches = $.grep(genres, function(g) { + return g.player_type === USER_TYPE && g.genre_type === PROFILE_GENRE_TYPE; + }); + + return matches; + } + + profileUtils.virtualBandGenres = function(genres) { + var matches = $.grep(genres, function(g) { + return g.player_type === USER_TYPE && g.genre_type === VIRTUAL_BAND_GENRE_TYPE; + }); + + return matches; + } + + profileUtils.traditionalBandGenres = function(genres) { + var matches = $.grep(genres, function(g) { + return g.player_type === USER_TYPE && g.genre_type === TRADITIONAL_BAND_GENRE_TYPE; + }); + + return matches; + } + + profileUtils.paidSessionGenres = function(genres) { + var matches = $.grep(genres, function(g) { + return g.player_type === USER_TYPE && g.genre_type === PAID_SESSION_GENRE_TYPE; + }); + + return matches; + } + + profileUtils.freeSessionGenres = function(genres) { + var matches = $.grep(genres, function(g) { + return g.player_type === USER_TYPE && g.genre_type === FREE_SESSION_GENRE_TYPE; + }); + + return matches; + } + + profileUtils.cowritingGenres = function(genres) { + var matches = $.grep(genres, function(g) { + return g.player_type === USER_TYPE && g.genre_type === COWRITING_GENRE_TYPE; + }); + + return matches; + } + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/profile.css.scss b/web/app/assets/stylesheets/client/profile.css.scss index c260b7e26..e656aa722 100644 --- a/web/app/assets/stylesheets/client/profile.css.scss +++ b/web/app/assets/stylesheets/client/profile.css.scss @@ -28,19 +28,17 @@ margin: 0px 0px 0px 0px; } -.profile-about-right .section { - font-weight:bold; +.profile-about-right .section-header { + font-weight:200; font-size:20px; float:left; - margin: 0px 0px 0px 0px; + margin: 0px 0px 10px 0px; } -.profile-status { +.profile-experience { font-size:12px; float:left; - display:inline-block; - vertical-align:middle; - line-height:30px; + width: 80px; } .profile-photo { @@ -165,7 +163,7 @@ font-weight:600; } -#profile-bands .when-empty { +#bands-content .when-empty { margin: 0px; padding:0px; display:block; @@ -176,7 +174,7 @@ line-height: 150%; } -#profile-bands .when-empty a { +#bands-content .when-empty a { text-decoration: underline; color: inherit; } @@ -336,7 +334,7 @@ display:none; } -#profile-history { +#history-content { padding:0 10px 0 20px; width:100%; position:relative; diff --git a/web/app/views/api_users/profile_show.rabl b/web/app/views/api_users/profile_show.rabl index 820a3809b..c88dfa048 100644 --- a/web/app/views/api_users/profile_show.rabl +++ b/web/app/views/api_users/profile_show.rabl @@ -17,4 +17,21 @@ end child :genre_players => :genres do attributes :genre_id, :player_type, :genre_type -end \ No newline at end of file +end + +child :band_musicians => :bands do + attributes :id, :name, :admin, :photo_url, :logo_url + + child :genres => :genres do + attributes :id, :description + #partial('api_genres/index', :object => @user.bands.genres) + end +end + +child :musician_instruments => :instruments do + attributes :description, :proficiency_level, :priority, :instrument_id +end + +# child :genres do +# attributes :description, :id +# end \ No newline at end of file diff --git a/web/app/views/clients/_profile.html.erb b/web/app/views/clients/_profile.html.erb index 64797c7e1..4a262170e 100644 --- a/web/app/views/clients/_profile.html.erb +++ b/web/app/views/clients/_profile.html.erb @@ -1,162 +1,195 @@
-
-
- <%= image_tag "content/icon_profile.png", :size => "19x19" %> +
+
+ <%= image_tag "content/icon_profile.png", :size => "19x19" %> +
+ +

musician profile

+ + <%= render "screen_navigation" %> +
+
+ +
+ +
+

+ <%= link_to("EDIT PROFILE", '/client#/account/profile', :id => "btn-edit", :class => "button-orange") %>
-

musician profile

+ + +

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

- <%= link_to("EDIT PROFILE", '/client#/account/profile', :id => "btn-profile-edit", :class => "button-orange") %> -
+ + +
+
- -
-
+
+
+ +
+
+

Location:


+
+

+

Stats:


+
+
+
+
+
+
+
+
+ +
Bio
+
+ +
+ Not specified. +
+ + +
- - -

- - -
-
- -
-
- - -
+
+

+
+ +
-
-
- -
-
-

Location:


-
-

-

Stats:


-
-
-
-
-
-
-
-
-
Bio
-
- Not specified. Edit Bio -
-
-

-
- -
-
-
-
-
-
-
-
-
- <%= form_tag('', {:id => 'user-feed-form', :class => 'inner-content'}) do %> - <%= render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_FEED, :id => 'user-feed-controls'}) %> -
-
-
-
-
No more feed entries
-
Loading ...
-
-
+
Musical Experience
+
+ +
+
+
+ +
+
Status:
+
+
+
+
Genres:
+
+
+
+
Concert Gigs:
+
+
+
+
Studio Gigs:
+
+
+ +
+
+ +
Performance Samples
+ +
+
+ +
Online Presence
+
+
+
+
+
+
+ <%= form_tag('', {:id => 'user-feed-form', :class => 'inner-content'}) do %> + <%= render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_FEED, :id => 'user-feed-controls'}) %> +
+
+
+
+
No more feed entries
+
Loading ...
+
+
+
+ <% end %> +
+
+
+
+
+
+
+

Friends

+
+
+

Following

+
+
+

Followers

+
+
+
+
+
+
+ +
+
+
- - <% end %> -
-
-
-
-
-
-
-

Friends

-
-
-

Following

-
-
-

Followers

-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- +
+
+
+
+
+
+
+
+
+
- -
+
+
+
+
+
+
+
+
+
+
+
+ +
- - - - From cba51df5e9bacb7b6d3c1da837fbc8f09fdc5ee3 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 4 Mar 2015 02:17:12 -0500 Subject: [PATCH 039/195] VRFS-2701 edit profile basics --- web/app/assets/stylesheets/client/account.css.scss | 5 +++++ web/app/views/clients/_account_profile.html.erb | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/web/app/assets/stylesheets/client/account.css.scss b/web/app/assets/stylesheets/client/account.css.scss index 965ed4036..df6551878 100644 --- a/web/app/assets/stylesheets/client/account.css.scss +++ b/web/app/assets/stylesheets/client/account.css.scss @@ -63,6 +63,11 @@ width: 50%; } + textarea.biography { + width: 80%; + height: 90px; + } + .account-profile { padding-top:20px; diff --git a/web/app/views/clients/_account_profile.html.erb b/web/app/views/clients/_account_profile.html.erb index 36ce8469f..a1a856a5d 100644 --- a/web/app/views/clients/_account_profile.html.erb +++ b/web/app/views/clients/_account_profile.html.erb @@ -85,7 +85,8 @@
- + +
From 98bf25e3606a935b6f26ce6cc70db03f14bc44b3 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 4 Mar 2015 02:27:32 -0500 Subject: [PATCH 040/195] VRFS-2701 edit profile basics --- web/app/assets/stylesheets/client/account.css.scss | 13 ++++++++----- web/app/views/clients/_account_profile.html.erb | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/web/app/assets/stylesheets/client/account.css.scss b/web/app/assets/stylesheets/client/account.css.scss index df6551878..445d07eec 100644 --- a/web/app/assets/stylesheets/client/account.css.scss +++ b/web/app/assets/stylesheets/client/account.css.scss @@ -63,11 +63,6 @@ width: 50%; } - textarea.biography { - width: 80%; - height: 90px; - } - .account-profile { padding-top:20px; @@ -89,6 +84,14 @@ margin-right:17%; } + textarea.biography { + width: 80%; + height: 90px; + } + + .actions { + margin-right: 125px; + } .location { position:relative; diff --git a/web/app/views/clients/_account_profile.html.erb b/web/app/views/clients/_account_profile.html.erb index a1a856a5d..7ea889540 100644 --- a/web/app/views/clients/_account_profile.html.erb +++ b/web/app/views/clients/_account_profile.html.erb @@ -93,7 +93,7 @@ I will accept email from JamKazam about this service.
- From 02e22ba7f32b39155d7156d31f0ef97f8e8c9027 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 5 Mar 2015 05:50:10 +0000 Subject: [PATCH 041/195] VRFS-2795 add interests, studios selectors; some refactoring --- .../musician_search_filter.js.coffee | 30 ++++++++++++++----- web/app/assets/javascripts/profile_utils.js | 12 ++++++-- web/app/controllers/spikes_controller.rb | 4 +++ .../clients/_musician_search_filter.html.slim | 2 +- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index be1cb3e71..7b5a63fc9 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -38,17 +38,15 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter content_root.html template this.populateSkill() + this.populateStudio() this.populateGigs() + this.populateInterests() # this.loadGenres() # this.loadInstruments() - _populateStruct: (sourceStruct, selection, element) => + _populateSelectWithKeys: (struct, selection, keys, element) => element.children().remove() - struct = - '-1': 'Any' - $.extend(struct, sourceStruct) - - $.each Object.keys(struct).sort(), (idx, value) => + $.each keys, (idx, value) => label = struct[value] blankOption = $ '' blankOption.text label @@ -56,13 +54,29 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter blankOption.attr 'selected', '' if value == selection element.append(blankOption) + _populateSelectWithInt: (sourceStruct, selection, element) => + struct = + '-1': 'Any' + $.extend(struct, sourceStruct) + this._populateSelectWithKeys(struct, selection, Object.keys(struct).sort(), element) + + populateInterests: () => + elem = $ '#musician-search-filter-builder select[name=interests]' + struct = gon.musician_search_interests['map'] + keys = gon.musician_search_interests['keys'] + this._populateSelectWithKeys(struct, @searchFilter.interests, keys, elem) + + populateStudio: () => + elem = $ '#musician-search-filter-builder select[name=studio_sessions]' + this._populateSelectWithInt(@profileUtils.studioMap, @searchFilter.studio_session_count.toString(), elem) + populateGigs: () => elem = $ '#musician-search-filter-builder select[name=concert_gigs]' - this._populateStruct(@profileUtils.gigMap, @searchFilter.gig_count.toString(), elem) + this._populateSelectWithInt(@profileUtils.gigMap, @searchFilter.gig_count.toString(), elem) populateSkill: () => elem = $ '#musician-search-filter-builder select[name=skill]' - this._populateStruct(@profileUtils.skillLevelMap, @searchFilter.skill_level.toString(), elem) + this._populateSelectWithInt(@profileUtils.skillLevelMap, @searchFilter.skill_level.toString(), elem) loadInstruments: () => diff --git a/web/app/assets/javascripts/profile_utils.js b/web/app/assets/javascripts/profile_utils.js index 6ee38beb0..cdaf0d6de 100644 --- a/web/app/assets/javascripts/profile_utils.js +++ b/web/app/assets/javascripts/profile_utils.js @@ -13,8 +13,8 @@ var PROFILE_GENRE_TYPE = 'profile'; var VIRTUAL_BAND_GENRE_TYPE = 'virtual_band'; var TRADITIONAL_BAND_GENRE_TYPE = 'traditional_band'; - var PAID_SESSION_GENRE_TYPE = 'paid_session'; - var FREE_SESSION_GENRE_TYPE = 'free_session'; + var PAID_SESSION_GENRE_TYPE = 'paid_sessions'; + var FREE_SESSION_GENRE_TYPE = 'free_sessions'; var COWRITING_GENRE_TYPE = 'cowriting'; // performance samples @@ -49,6 +49,14 @@ "4": "over 100" }; + profileUtils.studioMap = { + "0": "zero", + "1": "under 10", + "2": "10 to 50", + "3": "50 to 100", + "4": "over 100" + }; + profileUtils.cowritingPurposeMap = { "1": "just for fun", "2": "sell music" diff --git a/web/app/controllers/spikes_controller.rb b/web/app/controllers/spikes_controller.rb index a4fd2d10a..c6f7fde68 100644 --- a/web/app/controllers/spikes_controller.rb +++ b/web/app/controllers/spikes_controller.rb @@ -62,6 +62,10 @@ class SpikesController < ApplicationController end def musician_search_filter + gon.musician_search_interests = { + keys: MusicianSearch::INTEREST_VALS, + map: MusicianSearch::INTERESTS + } render :layout => 'web' end diff --git a/web/app/views/clients/_musician_search_filter.html.slim b/web/app/views/clients/_musician_search_filter.html.slim index 4aa340bb8..a613432f6 100644 --- a/web/app/views/clients/_musician_search_filter.html.slim +++ b/web/app/views/clients/_musician_search_filter.html.slim @@ -3,7 +3,7 @@ div#musician-search-filter-builder script#template-musician-search-filter type="text/template" .field label Interests: - select.w80 disabled="disabled" name="interests" + select.w80 name="interests" option selected="selected" value="{interests}" {interests) .field From d514a67be598b6936e7d85acec4214f0839393cc Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 5 Mar 2015 05:51:26 +0000 Subject: [PATCH 042/195] VRFS-2795 changed gig/studio vals to match profile enhancements branch --- ruby/lib/jam_ruby/models/musician_search.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ruby/lib/jam_ruby/models/musician_search.rb b/ruby/lib/jam_ruby/models/musician_search.rb index 849ffbc2a..d57ceaf09 100644 --- a/ruby/lib/jam_ruby/models/musician_search.rb +++ b/ruby/lib/jam_ruby/models/musician_search.rb @@ -29,7 +29,7 @@ module JamRuby SKILL_VALS[2] => 'Pro', } - GIG_COUNTS = [ANY_VAL_INT, 0, 1, 2, 3, ] + GIG_COUNTS = [ANY_VAL_INT, 0, 1, 2, 3, 4] GIG_LABELS = { GIG_COUNTS[0] => 'Any', GIG_COUNTS[1] => 'More than 0', @@ -42,7 +42,7 @@ module JamRuby (GIG_COUNTS[3]...GIG_COUNTS[4]), (GIG_COUNTS[4]...PG_SMALLINT_MAX)] - STUDIO_COUNTS = [ANY_VAL_INT, 1, 11, 51, 101] + STUDIO_COUNTS = [ANY_VAL_INT, 0, 1, 2, 3, 4] STUDIOS_LABELS = { STUDIO_COUNTS[0] => 'Any', STUDIO_COUNTS[1] => 'More than 0', From 2380c507794cfcc5b3b8d19e02a9f1afe942d4dc Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 5 Mar 2015 06:30:20 +0000 Subject: [PATCH 043/195] VRFS-2795 interests and ages selectors --- .../musician_search_filter.js.coffee | 29 ++++++++++++++++--- web/app/controllers/spikes_controller.rb | 12 ++++++-- .../clients/_musician_search_filter.html.slim | 16 ++++++---- 3 files changed, 44 insertions(+), 13 deletions(-) diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index 7b5a63fc9..386c0bf43 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -41,7 +41,8 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter this.populateStudio() this.populateGigs() this.populateInterests() - # this.loadGenres() + this.populateAges() + this.populateGenres() # this.loadInstruments() _populateSelectWithKeys: (struct, selection, keys, element) => @@ -62,8 +63,8 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter populateInterests: () => elem = $ '#musician-search-filter-builder select[name=interests]' - struct = gon.musician_search_interests['map'] - keys = gon.musician_search_interests['keys'] + struct = gon.musician_search_filter['interests']['map'] + keys = gon.musician_search_filter['interests']['keys'] this._populateSelectWithKeys(struct, @searchFilter.interests, keys, elem) populateStudio: () => @@ -78,9 +79,29 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter elem = $ '#musician-search-filter-builder select[name=skill]' this._populateSelectWithInt(@profileUtils.skillLevelMap, @searchFilter.skill_level.toString(), elem) + populateAges: () => + $('#search-filter-ages').empty() + $.each gon.musician_search_filter['ages']['keys'], (index, key) => + ageTemplate = $('#template-search-filter-setup-ages').html() + selected = '' + ageLabel = gon.musician_search_filter['ages']['map'][key] + if 0 < @searchFilter.ages.length + + ageMatch = $.grep(@searchFilter.ages, (n, i) -> + n.id == key + ) + if ageMatch.length > 0 + selected = 'checked' + ageHtml = context.JK.fillTemplate(ageTemplate, + id: key + description: ageLabel + checked: selected) + $('#search-filter-ages').append ageHtml + + loadInstruments: () => - loadGenres: () => + populateGenres: () => $('#search-filter-genres').empty() @rest.getGenres().done (genres) => $.each genres, (index, genre) => diff --git a/web/app/controllers/spikes_controller.rb b/web/app/controllers/spikes_controller.rb index c6f7fde68..01f466f70 100644 --- a/web/app/controllers/spikes_controller.rb +++ b/web/app/controllers/spikes_controller.rb @@ -62,9 +62,15 @@ class SpikesController < ApplicationController end def musician_search_filter - gon.musician_search_interests = { - keys: MusicianSearch::INTEREST_VALS, - map: MusicianSearch::INTERESTS + gon.musician_search_filter = { + interests: { + keys: MusicianSearch::INTEREST_VALS, + map: MusicianSearch::INTERESTS + }, + ages: { + keys: MusicianSearch::AGE_COUNTS, + map: MusicianSearch::AGES + } } render :layout => 'web' end diff --git a/web/app/views/clients/_musician_search_filter.html.slim b/web/app/views/clients/_musician_search_filter.html.slim index a613432f6..b92c413b7 100644 --- a/web/app/views/clients/_musician_search_filter.html.slim +++ b/web/app/views/clients/_musician_search_filter.html.slim @@ -26,16 +26,16 @@ script#template-musician-search-filter type="text/template" .profile-instrumentlist.w90 table.instrument_selector cellpadding="0" cellspacing="6" width="100%" + .field + label for="search-filter-ages" Ages: + .search-filter-setup-ages + table#search-filter-ages cellpadding="10" cellspacing="6" width="100%" + .field label for="search-filter-genres" Genres: .search-filter-setup-genres table#search-filter-genres cellpadding="10" cellspacing="6" width="100%" - .field - label for="filter-ages" Ages: - .filter-setup-ages - table#filter-ages cellpadding="10" cellspacing="6" width="100%" - script#musician-search-filter-instrument type="text/template" tr data-instrument-id="{id}" @@ -48,4 +48,8 @@ script#musician-search-filter-instrument type="text/template" script#template-search-filter-setup-genres type="text/template" tr - td {description + td {description} + +script#template-search-filter-setup-ages type="text/template" + tr + td {description} From 2e24b58e122096815b43a289454e72a166b798a6 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 5 Mar 2015 06:49:44 +0000 Subject: [PATCH 044/195] VRFS-2795 instrument selector --- .../musician_search_filter.js.coffee | 23 +++++++++++++++---- .../clients/_musician_search_filter.html.slim | 12 +++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index 386c0bf43..1e9c32084 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -43,7 +43,7 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter this.populateInterests() this.populateAges() this.populateGenres() - # this.loadInstruments() + this.populateInstruments() _populateSelectWithKeys: (struct, selection, keys, element) => element.children().remove() @@ -98,9 +98,6 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter checked: selected) $('#search-filter-ages').append ageHtml - - loadInstruments: () => - populateGenres: () => $('#search-filter-genres').empty() @rest.getGenres().done (genres) => @@ -118,3 +115,21 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter description: genre.description checked: selected) $('#search-filter-genres').append genreHtml + + populateInstruments: () => + $('#search-filter-instruments').empty() + @rest.getInstruments().done (instruments) => + $.each instruments, (index, instrument) => + instrumentTemplate = $('#template-search-filter-setup-instrument').html() + selected = '' + if 0 < @searchFilter.instruments.length + instrumentMatch = $.grep(@searchFilter.instruments, (n, i) -> + n.id == instrument.id + ) + if instrumentMatch.length > 0 + selected = 'checked' + instrumentHtml = context.JK.fillTemplate(instrumentTemplate, + id: instrument.id + description: instrument.description + checked: selected) + $('#search-filter-instruments').append instrumentHtml diff --git a/web/app/views/clients/_musician_search_filter.html.slim b/web/app/views/clients/_musician_search_filter.html.slim index b92c413b7..00d0fdcc9 100644 --- a/web/app/views/clients/_musician_search_filter.html.slim +++ b/web/app/views/clients/_musician_search_filter.html.slim @@ -22,10 +22,10 @@ script#template-musician-search-filter type="text/template" option selected="selected" value="{concert_gigs}" {concert_gigs) .field - | What instruments can you play? - .profile-instrumentlist.w90 - table.instrument_selector cellpadding="0" cellspacing="6" width="100%" - + label for="search-filter-instruments" Instruments: + .search-filter-setup-instruments + table#search-filter-instruments cellpadding="10" cellspacing="6" width="100%" + .field label for="search-filter-ages" Ages: .search-filter-setup-ages @@ -37,9 +37,9 @@ script#template-musician-search-filter type="text/template" table#search-filter-genres cellpadding="10" cellspacing="6" width="100%" -script#musician-search-filter-instrument type="text/template" +script#template-search-filter-setup-instrument type="text/template" tr data-instrument-id="{id}" - td {description + td {description} td align="right" width="50%" select.proficiency_selector name="proficiency" option value="1" Beginner From 58324f35a5c372c5afb88e6a1b72085e159b78a4 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Thu, 5 Mar 2015 02:16:32 -0500 Subject: [PATCH 045/195] VRFS-2854 fixed bug with hovering over private sessions in feed --- .../javascripts/jquery.listenbroadcast.js | 104 +++++++++++------- 1 file changed, 65 insertions(+), 39 deletions(-) diff --git a/web/app/assets/javascripts/jquery.listenbroadcast.js b/web/app/assets/javascripts/jquery.listenbroadcast.js index 73837da8f..00f87fcbe 100644 --- a/web/app/assets/javascripts/jquery.listenbroadcast.js +++ b/web/app/assets/javascripts/jquery.listenbroadcast.js @@ -92,32 +92,38 @@ .done(function(response) { if(!sessionInfo.mount) { - transition(PlayStateSessionOver); - destroy(); + if (sessionInfo.session_removed_at) { + transition(PlayStateSessionOver); + destroy(); + } } else { - recreateAudioElement(); + try { + recreateAudioElement(); - audioDomElement.load(); + audioDomElement.load(); - retryAttempts = 0; + retryAttempts = 0; - transition(PlayStateInitializing); + transition(PlayStateInitializing); - // keep this after transition, because any transition clears this timer - waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT); - logger.debug("setting buffering timeout"); - rest.addPlayablePlay(musicSessionId, 'JamRuby::MusicSession', null, context.JK.currentUserId); + // keep this after transition, because any transition clears this timer + waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT); + logger.debug("setting buffering timeout"); + rest.addPlayablePlay(musicSessionId, 'JamRuby::MusicSession', null, context.JK.currentUserId); - if(needsCanPlayGuard()) { - $audio.bind('canplay', function() { + if(needsCanPlayGuard()) { + $audio.bind('canplay', function() { + audioDomElement.play(); + }) + } + else { audioDomElement.play(); - }) + } } - else { - audioDomElement.play(); + catch (err) { + console.log("Catching error = %o", err); } - } }) } @@ -137,7 +143,12 @@ transition(PlayStateNone); - recreateAudioElement(); + try { + recreateAudioElement(); + } + catch (err) { + console.log("Catching error = %o", err); + } } function destroy() { @@ -145,7 +156,12 @@ //$audio.remove(); //$audio = null; //audioDomElement = null; - recreateAudioElement() + try { + recreateAudioElement(); + } + catch (err) { + console.log("Catching error = %o", err); + } // destroyed = true; //} } @@ -249,31 +265,37 @@ .done(function(response) { if(!sessionInfo.mount) { - transition(PlayStateSessionOver); - destroy(); + if (sessionInfo.session_removed_at) { + transition(PlayStateSessionOver); + destroy(); + } } else { // tell audio to stop/start, in attempt to retry //audioDomElement.pause(); - recreateAudioElement(); + try { + recreateAudioElement(); - audioDomElement.load(); - if(needsCanPlayGuard()) { - $audio.bind('canplay', function() { + audioDomElement.load(); + if(needsCanPlayGuard()) { + $audio.bind('canplay', function() { + audioDomElement.play(); + }) + } + else { audioDomElement.play(); - }) - } - else { - audioDomElement.play(); - } + } - transition(PlayStateRetrying); + transition(PlayStateRetrying); - waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT); + waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT); + } + catch (err) { + console.log("Catching error = %o", err); + } } - }) - + }); } } @@ -483,9 +505,11 @@ checkServer() .done(function(response) { if(!sessionInfo.mount) { - transition(PlayStateSessionOver); - destroy(); - }}) + if (sessionInfo.session_removed_at) { + transition(PlayStateSessionOver); + destroy(); + } + }}); } } @@ -694,10 +718,12 @@ else { mountInfo = null; - transition(PlayStateSessionOver); - destroy(); + if (sessionInfo.session_removed_at) { + transition(PlayStateSessionOver); + destroy(); + } - context.JK.app.layout.notify('This session can not currently broadcast') + context.JK.app.notify({"title": "Unable to Broadcast Session", "text": "This session cannot be broadcasted. The session organizer may have configured it to be private."}); } }) .fail(function() { From 81df46e7a283d4bb8d5d0c2494b0ccf29d1e1ede Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Fri, 6 Mar 2015 05:10:29 +0000 Subject: [PATCH 046/195] VRFS-2795 tweaks --- web/app/assets/javascripts/musician_search_filter.js.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index 1e9c32084..5eab48686 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -81,10 +81,11 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter populateAges: () => $('#search-filter-ages').empty() + ages_map = gon.musician_search_filter['ages']['map'] $.each gon.musician_search_filter['ages']['keys'], (index, key) => ageTemplate = $('#template-search-filter-setup-ages').html() selected = '' - ageLabel = gon.musician_search_filter['ages']['map'][key] + ageLabel = ages_map[key] if 0 < @searchFilter.ages.length ageMatch = $.grep(@searchFilter.ages, (n, i) -> From 15559f593f2b24ccb866578e25e9d6ae9561690c Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Fri, 6 Mar 2015 06:33:38 +0000 Subject: [PATCH 047/195] VRFS-2795 sortorder, refactoring --- ruby/lib/jam_ruby/models/musician_search.rb | 5 ++- .../musician_search_filter.js.coffee | 42 ++++++++++++++----- web/app/controllers/spikes_controller.rb | 5 +++ .../clients/_musician_search_filter.html.slim | 11 ++++- 4 files changed, 48 insertions(+), 15 deletions(-) diff --git a/ruby/lib/jam_ruby/models/musician_search.rb b/ruby/lib/jam_ruby/models/musician_search.rb index d57ceaf09..e99c02549 100644 --- a/ruby/lib/jam_ruby/models/musician_search.rb +++ b/ruby/lib/jam_ruby/models/musician_search.rb @@ -7,8 +7,8 @@ module JamRuby PER_PAGE = 10 PG_SMALLINT_MAX = 32767 - KEY_GIGS = 'gig_count' - KEY_STUDIOS = 'studio_session_count' + KEY_GIGS = 'concert_gigs' + KEY_STUDIOS = 'studio_sessions' KEY_AGES = 'ages' KEY_SKILL = 'skill_level' KEY_GENRES = 'genres' @@ -91,6 +91,7 @@ module JamRuby KEY_SKILL => SKILL_VALS[0], KEY_AGES => [AGE_COUNTS[0]] } + JSON_SCHEMA_KEYS = JSON_SCHEMA.keys def self.user_search_filter(user) unless ms = user.musician_search diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index 5eab48686..474c9fb56 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -13,6 +13,9 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter init: () => @screenBindings = { 'beforeShow': this.beforeShow, 'afterShow': this.afterShow } + srchbtn = $ '#btn-perform-musician-search' + srchbtn.on 'click', => + this.performSearch() renderSearchFilter: () => $.when(this.rest.getMusicianSearchFilter()).done (sFilter) => @@ -28,13 +31,13 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter @searchFilter = sFilter args = interests: @searchFilter.interests - skill: @searchFilter.skill_level - studio_sessions: @searchFilter.studio_session_count - concert_gigs: @searchFilter.gig_count + skill_level: @searchFilter.skill_level + studio_sessions: @searchFilter.studio_sessions + concert_gigs: @searchFilter.concert_gigs template = context.JK.fillTemplate($('#template-musician-search-filter').html(), args) - content_root = $('#musician-search-filter-builder') + content_root = $('#musician-search-filter-builder-form') content_root.html template this.populateSkill() @@ -44,6 +47,7 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter this.populateAges() this.populateGenres() this.populateInstruments() + this.populateSortOrder() _populateSelectWithKeys: (struct, selection, keys, element) => element.children().remove() @@ -55,28 +59,34 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter blankOption.attr 'selected', '' if value == selection element.append(blankOption) + _populateSelectIdentifier: (identifier) => + elem = $ '#musician-search-filter-builder select[name='+identifier+']' + struct = gon.musician_search_filter[identifier]['map'] + keys = gon.musician_search_filter[identifier]['keys'] + this._populateSelectWithKeys(struct, @searchFilter[identifier], keys, elem) + _populateSelectWithInt: (sourceStruct, selection, element) => struct = '-1': 'Any' $.extend(struct, sourceStruct) this._populateSelectWithKeys(struct, selection, Object.keys(struct).sort(), element) + populateSortOrder: () => + this._populateSelectIdentifier('sort_order') + populateInterests: () => - elem = $ '#musician-search-filter-builder select[name=interests]' - struct = gon.musician_search_filter['interests']['map'] - keys = gon.musician_search_filter['interests']['keys'] - this._populateSelectWithKeys(struct, @searchFilter.interests, keys, elem) + this._populateSelectIdentifier('interests') populateStudio: () => elem = $ '#musician-search-filter-builder select[name=studio_sessions]' - this._populateSelectWithInt(@profileUtils.studioMap, @searchFilter.studio_session_count.toString(), elem) + this._populateSelectWithInt(@profileUtils.studioMap, @searchFilter.studio_sessions.toString(), elem) populateGigs: () => elem = $ '#musician-search-filter-builder select[name=concert_gigs]' - this._populateSelectWithInt(@profileUtils.gigMap, @searchFilter.gig_count.toString(), elem) + this._populateSelectWithInt(@profileUtils.gigMap, @searchFilter.concert_gigs.toString(), elem) populateSkill: () => - elem = $ '#musician-search-filter-builder select[name=skill]' + elem = $ '#musician-search-filter-builder select[name=skill_level]' this._populateSelectWithInt(@profileUtils.skillLevelMap, @searchFilter.skill_level.toString(), elem) populateAges: () => @@ -134,3 +144,13 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter description: instrument.description checked: selected) $('#search-filter-instruments').append instrumentHtml + + _selectedValue: (identifier) => + elem = $ '#musician-search-filter-builder select[name='+identifier+']' + elem.val() + + performSearch: () => + $.each gon.musician_search_filter['filter_keys'], (index, key) => + @searchFilter[key] = this._selectedValue('sort_order') + @logger.debug("performSearch: "+JSON.stringify(@searchFilter)) + \ No newline at end of file diff --git a/web/app/controllers/spikes_controller.rb b/web/app/controllers/spikes_controller.rb index 01f466f70..c1e012283 100644 --- a/web/app/controllers/spikes_controller.rb +++ b/web/app/controllers/spikes_controller.rb @@ -63,6 +63,11 @@ class SpikesController < ApplicationController def musician_search_filter gon.musician_search_filter = { + filter_keys: MusicianSearch::JSON_SCHEMA_KEYS, + sort_order: { + keys: MusicianSearch::SORT_VALS, + map: MusicianSearch::SORT_ORDERS + }, interests: { keys: MusicianSearch::INTEREST_VALS, map: MusicianSearch::INTERESTS diff --git a/web/app/views/clients/_musician_search_filter.html.slim b/web/app/views/clients/_musician_search_filter.html.slim index 00d0fdcc9..1d359423e 100644 --- a/web/app/views/clients/_musician_search_filter.html.slim +++ b/web/app/views/clients/_musician_search_filter.html.slim @@ -1,6 +1,13 @@ div#musician-search-filter-builder + a#btn-perform-musician-search.button-grey href="#" SEARCH + div#musician-search-filter-builder-form script#template-musician-search-filter type="text/template" + .field + label Sort Results By: + select.w80 name="sort_order" + option selected="selected" value="{sort_order}" {sort_order) + .field label Interests: select.w80 name="interests" @@ -8,8 +15,8 @@ script#template-musician-search-filter type="text/template" .field label Skill: - select.w80 name="skill" - option selected="selected" value="{skill}" {skill_label) + select.w80 name="skill_level" + option selected="selected" value="{skill_level}" {skill_label) .field label Studio Sessions Played: From 2b9bebb4380d9092ea9f72841ba2726ffee19f83 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Fri, 6 Mar 2015 07:03:30 +0000 Subject: [PATCH 048/195] VRFS-2795 generate JSON from filter settings --- ruby/lib/jam_ruby/models/musician_search.rb | 2 ++ .../musician_search_filter.js.coffee | 17 +++++++++++++++-- web/app/controllers/spikes_controller.rb | 6 +++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/ruby/lib/jam_ruby/models/musician_search.rb b/ruby/lib/jam_ruby/models/musician_search.rb index e99c02549..2df73822b 100644 --- a/ruby/lib/jam_ruby/models/musician_search.rb +++ b/ruby/lib/jam_ruby/models/musician_search.rb @@ -92,6 +92,8 @@ module JamRuby KEY_AGES => [AGE_COUNTS[0]] } JSON_SCHEMA_KEYS = JSON_SCHEMA.keys + MULTI_VALUE_KEYS = JSON_SCHEMA.collect { |kk,vv| vv.is_a?(Array) ? kk : nil }.compact + SINGLE_VALUE_KEYS = JSON_SCHEMA.keys - MULTI_VALUE_KEYS def self.user_search_filter(user) unless ms = user.musician_search diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index 474c9fb56..3b2969d33 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -149,8 +149,21 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter elem = $ '#musician-search-filter-builder select[name='+identifier+']' elem.val() + _selectedMultiValue: (identifier) => + if 'instruments' == identifier + [] + else + elem = $ '#musician-search-filter-builder select[name='+identifier+'] input[type=checkbox]:checked' + vals = [] + elem.each (idx) -> + vals.push($ this.val()) + vals + performSearch: () => - $.each gon.musician_search_filter['filter_keys'], (index, key) => - @searchFilter[key] = this._selectedValue('sort_order') + $.each gon.musician_search_filter.filter_keys.single, (index, key) => + @searchFilter[key] = this._selectedValue(key) + $.each gon.musician_search_filter.filter_keys.multi, (index, key) => + @searchFilter[key] = this._selectedMultiValue(key) + @logger.debug("performSearch: "+JSON.stringify(@searchFilter)) \ No newline at end of file diff --git a/web/app/controllers/spikes_controller.rb b/web/app/controllers/spikes_controller.rb index c1e012283..b43df2dee 100644 --- a/web/app/controllers/spikes_controller.rb +++ b/web/app/controllers/spikes_controller.rb @@ -63,7 +63,11 @@ class SpikesController < ApplicationController def musician_search_filter gon.musician_search_filter = { - filter_keys: MusicianSearch::JSON_SCHEMA_KEYS, + filter_keys: { + keys: MusicianSearch::JSON_SCHEMA_KEYS, + multi: MusicianSearch::MULTI_VALUE_KEYS, + single: MusicianSearch::SINGLE_VALUE_KEYS, + }, sort_order: { keys: MusicianSearch::SORT_VALS, map: MusicianSearch::SORT_ORDERS From 3549ea117ccadc54bc7949a266062937e251067f Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Fri, 6 Mar 2015 07:20:14 +0000 Subject: [PATCH 049/195] VRFS-2795 generating json from filter settings --- ruby/lib/jam_ruby/models/musician_search.rb | 23 ++++++++++++++++--- .../musician_search_filter.js.coffee | 4 ++-- web/app/controllers/spikes_controller.rb | 20 +--------------- .../clients/_musician_search_filter.html.slim | 9 ++++---- 4 files changed, 27 insertions(+), 29 deletions(-) diff --git a/ruby/lib/jam_ruby/models/musician_search.rb b/ruby/lib/jam_ruby/models/musician_search.rb index 2df73822b..56f0b1fe3 100644 --- a/ruby/lib/jam_ruby/models/musician_search.rb +++ b/ruby/lib/jam_ruby/models/musician_search.rb @@ -86,15 +86,32 @@ module JamRuby KEY_INSTRUMENTS => [], KEY_INTERESTS => INTEREST_VALS[0], KEY_GENRES => [], - KEY_GIGS => GIG_COUNTS[0], - KEY_STUDIOS => STUDIO_COUNTS[0], - KEY_SKILL => SKILL_VALS[0], + KEY_GIGS => GIG_COUNTS[0].to_s, + KEY_STUDIOS => STUDIO_COUNTS[0].to_s, + KEY_SKILL => SKILL_VALS[0].to_s, KEY_AGES => [AGE_COUNTS[0]] } JSON_SCHEMA_KEYS = JSON_SCHEMA.keys MULTI_VALUE_KEYS = JSON_SCHEMA.collect { |kk,vv| vv.is_a?(Array) ? kk : nil }.compact SINGLE_VALUE_KEYS = JSON_SCHEMA.keys - MULTI_VALUE_KEYS + SEARCH_FILTER_CONFIG = { + filter_keys: { + keys: JSON_SCHEMA_KEYS, + multi: MULTI_VALUE_KEYS, + single: SINGLE_VALUE_KEYS, + }, + sort_order: { + keys: SORT_VALS, map: SORT_ORDERS + }, + interests: { + keys: INTEREST_VALS, map: INTERESTS + }, + ages: { + keys: AGE_COUNTS, map: AGES + } + } + def self.user_search_filter(user) unless ms = user.musician_search ms = self.create_search(user) diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index 3b2969d33..ed6d2fcb9 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -153,10 +153,10 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter if 'instruments' == identifier [] else - elem = $ '#musician-search-filter-builder select[name='+identifier+'] input[type=checkbox]:checked' + elem = $ '#search-filter-'+identifier+' input[type=checkbox]:checked' vals = [] elem.each (idx) -> - vals.push($ this.val()) + vals.push($(this).val()) vals performSearch: () => diff --git a/web/app/controllers/spikes_controller.rb b/web/app/controllers/spikes_controller.rb index b43df2dee..4d27974de 100644 --- a/web/app/controllers/spikes_controller.rb +++ b/web/app/controllers/spikes_controller.rb @@ -62,25 +62,7 @@ class SpikesController < ApplicationController end def musician_search_filter - gon.musician_search_filter = { - filter_keys: { - keys: MusicianSearch::JSON_SCHEMA_KEYS, - multi: MusicianSearch::MULTI_VALUE_KEYS, - single: MusicianSearch::SINGLE_VALUE_KEYS, - }, - sort_order: { - keys: MusicianSearch::SORT_VALS, - map: MusicianSearch::SORT_ORDERS - }, - interests: { - keys: MusicianSearch::INTEREST_VALS, - map: MusicianSearch::INTERESTS - }, - ages: { - keys: MusicianSearch::AGE_COUNTS, - map: MusicianSearch::AGES - } - } + gon.musician_search_filter = MusicianSearch::SEARCH_FILTER_CONFIG render :layout => 'web' end diff --git a/web/app/views/clients/_musician_search_filter.html.slim b/web/app/views/clients/_musician_search_filter.html.slim index 1d359423e..0e5ba7e5b 100644 --- a/web/app/views/clients/_musician_search_filter.html.slim +++ b/web/app/views/clients/_musician_search_filter.html.slim @@ -28,11 +28,6 @@ script#template-musician-search-filter type="text/template" select.w80 name="concert_gigs" option selected="selected" value="{concert_gigs}" {concert_gigs) - .field - label for="search-filter-instruments" Instruments: - .search-filter-setup-instruments - table#search-filter-instruments cellpadding="10" cellspacing="6" width="100%" - .field label for="search-filter-ages" Ages: .search-filter-setup-ages @@ -43,6 +38,10 @@ script#template-musician-search-filter type="text/template" .search-filter-setup-genres table#search-filter-genres cellpadding="10" cellspacing="6" width="100%" + .field + label for="search-filter-instruments" Instruments: + .search-filter-setup-instruments + table#search-filter-instruments cellpadding="10" cellspacing="6" width="100%" script#template-search-filter-setup-instrument type="text/template" tr data-instrument-id="{id}" From 36a9f1f6a3b20d28a3e2a40ae7873131e7f8552c Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Fri, 6 Mar 2015 23:16:27 -0500 Subject: [PATCH 050/195] VRFS-2854 more session widget feed fixes --- .../javascripts/jquery.listenbroadcast.js | 60 +++++++++---------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/web/app/assets/javascripts/jquery.listenbroadcast.js b/web/app/assets/javascripts/jquery.listenbroadcast.js index 00f87fcbe..059b7c18b 100644 --- a/web/app/assets/javascripts/jquery.listenbroadcast.js +++ b/web/app/assets/javascripts/jquery.listenbroadcast.js @@ -92,10 +92,8 @@ .done(function(response) { if(!sessionInfo.mount) { - if (sessionInfo.session_removed_at) { - transition(PlayStateSessionOver); - destroy(); - } + transition(PlayStateSessionOver); + destroy(); } else { try { @@ -122,7 +120,7 @@ } } catch (err) { - console.log("Catching error = %o", err); + logger.log("Catching error = %o", err); } } }) @@ -147,7 +145,7 @@ recreateAudioElement(); } catch (err) { - console.log("Catching error = %o", err); + logger.log("Catching error = %o", err); } } @@ -160,7 +158,7 @@ recreateAudioElement(); } catch (err) { - console.log("Catching error = %o", err); + logger.log("Catching error = %o", err); } // destroyed = true; //} @@ -173,14 +171,21 @@ } function createAudioElementHtml() { - if (sessionInfo == null) throw "no session info"; - if (sessionInfo.mount == null) throw "no session mount info"; + if (sessionInfo == null) { + logger.log("no session info"); + throw "no session info"; + } + + if (sessionInfo.mount == null) { + logger.log("no session mount info"); + throw "no session mount info"; + } $audio = $('') - $parent.append($audio) + ''); + $parent.append($audio); audioDomElement = $audio.get(0); audioBind(); @@ -265,10 +270,8 @@ .done(function(response) { if(!sessionInfo.mount) { - if (sessionInfo.session_removed_at) { - transition(PlayStateSessionOver); - destroy(); - } + transition(PlayStateSessionOver); + destroy(); } else { // tell audio to stop/start, in attempt to retry @@ -292,7 +295,7 @@ waitForBufferingTimeout = setTimeout(noBuffer, WAIT_FOR_BUFFER_TIMEOUT); } catch (err) { - console.log("Catching error = %o", err); + logger.log("Catching error = %o", err); } } }); @@ -400,7 +403,7 @@ function checkServer() { return rest.getSession(musicSessionId) .done(function(response) { - console.log("assigning sessionInfo") + logger.log("assigning sessionInfo") sessionInfo = response; }) .fail(function(jqXHR) { @@ -505,10 +508,8 @@ checkServer() .done(function(response) { if(!sessionInfo.mount) { - if (sessionInfo.session_removed_at) { - transition(PlayStateSessionOver); - destroy(); - } + transition(PlayStateSessionOver); + destroy(); }}); } } @@ -703,26 +704,21 @@ function openBubble() { checkServer().done(function(response) { - var mountId = sessionInfo.mount ? sessionInfo.mount.id : null + var mountId = sessionInfo.mount ? sessionInfo.mount.id : null; if(mountId) { rest.getMount({id: mountId}) .done(function (mount) { - mountInfo = mount - $parent.data('mount-id', mountId) - context.JK.SubscriptionUtils.subscribe('mount', mountId).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, onDetailEvent) - $parent.btOn() + mountInfo = mount; + $parent.data('mount-id', mountId); + context.JK.SubscriptionUtils.subscribe('mount', mountId).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, onDetailEvent); + $parent.btOn(); }) .fail(context.JK.app.ajaxError) } else { mountInfo = null; - - if (sessionInfo.session_removed_at) { - transition(PlayStateSessionOver); - destroy(); - } - + destroy(); context.JK.app.notify({"title": "Unable to Broadcast Session", "text": "This session cannot be broadcasted. The session organizer may have configured it to be private."}); } }) From 03321a816693dea8ca7dc0672471e247d657e9c2 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Sat, 7 Mar 2015 06:19:14 +0000 Subject: [PATCH 051/195] VRFS-2795 instrument json --- ruby/lib/jam_ruby/models/musician_search.rb | 19 +++++++++---------- .../musician_search_filter.js.coffee | 19 +++++++++++++------ web/app/controllers/api_search_controller.rb | 5 +++-- 3 files changed, 25 insertions(+), 18 deletions(-) diff --git a/ruby/lib/jam_ruby/models/musician_search.rb b/ruby/lib/jam_ruby/models/musician_search.rb index 56f0b1fe3..e4e4d1b14 100644 --- a/ruby/lib/jam_ruby/models/musician_search.rb +++ b/ruby/lib/jam_ruby/models/musician_search.rb @@ -96,27 +96,26 @@ module JamRuby SINGLE_VALUE_KEYS = JSON_SCHEMA.keys - MULTI_VALUE_KEYS SEARCH_FILTER_CONFIG = { + per_page: PER_PAGE, filter_keys: { keys: JSON_SCHEMA_KEYS, multi: MULTI_VALUE_KEYS, single: SINGLE_VALUE_KEYS, }, - sort_order: { - keys: SORT_VALS, map: SORT_ORDERS - }, - interests: { - keys: INTEREST_VALS, map: INTERESTS - }, - ages: { - keys: AGE_COUNTS, map: AGES - } + sort_order: { keys: SORT_VALS, map: SORT_ORDERS }, + interests: { keys: INTEREST_VALS, map: INTERESTS }, + ages: { keys: AGE_COUNTS, map: AGES } } def self.user_search_filter(user) unless ms = user.musician_search ms = self.create_search(user) end - ms.json + ms + end + + def self.search_filter_json(user) + self.user_search_filter(user).json end def self.create_search(user) diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index ed6d2fcb9..3a444ea12 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -150,20 +150,27 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter elem.val() _selectedMultiValue: (identifier) => + vals = [] + elem = $ '#search-filter-'+identifier+' input[type=checkbox]:checked' if 'instruments' == identifier - [] - else - elem = $ '#search-filter-'+identifier+' input[type=checkbox]:checked' - vals = [] elem.each (idx) -> - vals.push($(this).val()) - vals + row = $(this).parent().parent() + instrument = + instrument_id: row.data('instrument-id') + proficiency_level: row.find('select').val() + vals.push instrument + else + elem.each (idx) -> + vals.push $(this).val() + vals performSearch: () => $.each gon.musician_search_filter.filter_keys.single, (index, key) => @searchFilter[key] = this._selectedValue(key) + $.each gon.musician_search_filter.filter_keys.multi, (index, key) => @searchFilter[key] = this._selectedMultiValue(key) @logger.debug("performSearch: "+JSON.stringify(@searchFilter)) + @rest.postMusicianSearchFilter { filter: @searchFilter } \ No newline at end of file diff --git a/web/app/controllers/api_search_controller.rb b/web/app/controllers/api_search_controller.rb index e1ca4903c..459ef5374 100644 --- a/web/app/controllers/api_search_controller.rb +++ b/web/app/controllers/api_search_controller.rb @@ -24,11 +24,12 @@ class ApiSearchController < ApiController def musicians if request.get? - render :json => MusicianSearch.user_search_filter(current_user), :status => 200 + render :json => MusicianSearch.search_filter_json(current_user), :status => 200 elsif request.post? ms = MusicianSearch.user_search_filter(current_user) - render :json => ms.json_search_results(params), :status => 200 + results = ms.search_results_page(params[:filter]) + render :json => results, :status => 200 end end From 23ccc855d502b5b45a19f871fbcc6401ba57c0c8 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Sun, 8 Mar 2015 06:07:30 +0000 Subject: [PATCH 052/195] VRFS-2795 instrument selection; json posting --- web/app/assets/javascripts/jam_rest.js | 3 ++- .../musician_search_filter.js.coffee | 26 ++++++++++++------- web/app/controllers/api_search_controller.rb | 4 ++- .../clients/_musician_search_filter.html.slim | 4 +-- 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 390aba7ac..341878d2a 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -1584,7 +1584,8 @@ function postMusicianSearchFilter(query) { return $.ajax({ type: "POST", - url: "/api/search/musicians.json?" + $.param(query) + url: "/api/search/musicians.json", + data: query }); } diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index 3a444ea12..92594fa28 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -97,9 +97,9 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter selected = '' ageLabel = ages_map[key] if 0 < @searchFilter.ages.length - + key_val = key.toString() ageMatch = $.grep(@searchFilter.ages, (n, i) -> - n.id == key + n == key_val ) if ageMatch.length > 0 selected = 'checked' @@ -117,7 +117,7 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter selected = '' if 0 < @searchFilter.genres.length genreMatch = $.grep(@searchFilter.genres, (n, i) -> - n.id == genre.id + n == genre.id ) if genreMatch.length > 0 selected = 'checked' @@ -133,17 +133,24 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter $.each instruments, (index, instrument) => instrumentTemplate = $('#template-search-filter-setup-instrument').html() selected = '' + proficiency = '1' if 0 < @searchFilter.instruments.length - instrumentMatch = $.grep(@searchFilter.instruments, (n, i) -> - n.id == instrument.id + instMatch = $.grep(@searchFilter.instruments, (inst, i) -> + yn = inst.instrument_id == instrument.id + proficiency = inst.proficiency_level if yn + yn ) - if instrumentMatch.length > 0 - selected = 'checked' + if instMatch.length > 0 + selected = 'checked' instrumentHtml = context.JK.fillTemplate(instrumentTemplate, id: instrument.id description: instrument.description - checked: selected) + checked: selected + ) $('#search-filter-instruments').append instrumentHtml + instsel = '#search-filter-instruments tr[data-instrument-id="'+instrument.id+'"] select' + $(instsel).val(proficiency) + return true _selectedValue: (identifier) => elem = $ '#musician-search-filter-builder select[name='+identifier+']' @@ -171,6 +178,5 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter $.each gon.musician_search_filter.filter_keys.multi, (index, key) => @searchFilter[key] = this._selectedMultiValue(key) - @logger.debug("performSearch: "+JSON.stringify(@searchFilter)) - @rest.postMusicianSearchFilter { filter: @searchFilter } + @rest.postMusicianSearchFilter { filter: JSON.stringify(@searchFilter), page: 1 } \ No newline at end of file diff --git a/web/app/controllers/api_search_controller.rb b/web/app/controllers/api_search_controller.rb index 459ef5374..69fe64db6 100644 --- a/web/app/controllers/api_search_controller.rb +++ b/web/app/controllers/api_search_controller.rb @@ -27,8 +27,10 @@ class ApiSearchController < ApiController render :json => MusicianSearch.search_filter_json(current_user), :status => 200 elsif request.post? + logger.debug("*** params = #{params.inspect}") ms = MusicianSearch.user_search_filter(current_user) - results = ms.search_results_page(params[:filter]) + json = JSON.parse(params[:filter], :create_additions => false) + results = ms.search_results_page(json, [params[:page].to_i, 1].max) render :json => results, :status => 200 end end diff --git a/web/app/views/clients/_musician_search_filter.html.slim b/web/app/views/clients/_musician_search_filter.html.slim index 0e5ba7e5b..52f31775b 100644 --- a/web/app/views/clients/_musician_search_filter.html.slim +++ b/web/app/views/clients/_musician_search_filter.html.slim @@ -45,8 +45,8 @@ script#template-musician-search-filter type="text/template" script#template-search-filter-setup-instrument type="text/template" tr data-instrument-id="{id}" - td {description} - td align="right" width="50%" + td {description} + td align="right" width="50%" id="{proficiency_id}" select.proficiency_selector name="proficiency" option value="1" Beginner option value="2" Intermediate From 8bbfdbc013a7dfb5a6987ca525c5f842d2b2fef9 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Sun, 8 Mar 2015 06:13:29 +0000 Subject: [PATCH 053/195] VRFS-2795 instrument selection; json posting --- ruby/lib/jam_ruby/models/musician_search.rb | 8 +++++--- .../assets/javascripts/musician_search_filter.js.coffee | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ruby/lib/jam_ruby/models/musician_search.rb b/ruby/lib/jam_ruby/models/musician_search.rb index e4e4d1b14..a18fdd4d4 100644 --- a/ruby/lib/jam_ruby/models/musician_search.rb +++ b/ruby/lib/jam_ruby/models/musician_search.rb @@ -248,9 +248,11 @@ module JamRuby rel end - def search_results_page(params={}) - rel = do_search(params) - rel, page = self.pagination(rel, params) + def search_results_page(filter={}, page=1) + self.data_blob = filter + self.save + rel = do_search(filter) + rel, page = self.pagination(rel, filter) rel.to_json end diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index 92594fa28..a01c11886 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -148,8 +148,8 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter checked: selected ) $('#search-filter-instruments').append instrumentHtml - instsel = '#search-filter-instruments tr[data-instrument-id="'+instrument.id+'"] select' - $(instsel).val(proficiency) + profsel = '#search-filter-instruments tr[data-instrument-id="'+instrument.id+'"] select' + $(profsel).val(proficiency) return true _selectedValue: (identifier) => From 1cb887b696748878c1af677c0501464ea41d12b9 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Sun, 8 Mar 2015 08:17:36 +0000 Subject: [PATCH 054/195] VRFS-2795 spec testing --- .../fixtures/musicianSearch.html.slim | 4 +++ .../musician_search_filter_spec.js.coffee | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 web/spec/javascripts/fixtures/musicianSearch.html.slim create mode 100644 web/spec/javascripts/musician_search_filter_spec.js.coffee diff --git a/web/spec/javascripts/fixtures/musicianSearch.html.slim b/web/spec/javascripts/fixtures/musicianSearch.html.slim new file mode 100644 index 000000000..587b23540 --- /dev/null +++ b/web/spec/javascripts/fixtures/musicianSearch.html.slim @@ -0,0 +1,4 @@ +# = javascript_include_tag "profile_utils" +# = javascript_include_tag "musician_search_filter" +# = stylesheet_link_tag "client/musician" += render "clients/musician_search_filter" diff --git a/web/spec/javascripts/musician_search_filter_spec.js.coffee b/web/spec/javascripts/musician_search_filter_spec.js.coffee new file mode 100644 index 000000000..5b2708981 --- /dev/null +++ b/web/spec/javascripts/musician_search_filter_spec.js.coffee @@ -0,0 +1,36 @@ +describe "MusicianSearchFilter", -> + fixture.preload("musicianSearch.html") + + beforeEach => + @server = sinon.fakeServer.create(); + window.jamClient = sinon.stub() + @fixtures = fixture.load("musicianSearch.html", true) + window.gon = {} + window.gon.isNativeClient = true + @defaultFilter = {"per_page":10,"filter_keys":{"keys":["sort_order","instruments","interests","genres","concert_gigs","studio_sessions","skill_level","ages"],"multi":["instruments","genres","ages"],"single":["sort_order","interests","concert_gigs","studio_sessions","skill_level"]},"sort_order":{"keys":["latency","distance"],"map":{"latency":"Latency to Me","distance":"Distance to Me"}},"interests":{"keys":["any","virtual_band","traditional_band","paid_sessions","free_sessions","cowriting"],"map":{"any":"Any","virtual_band":"Virtual Band","traditional_band":"Traditional Band","paid_sessions":"Paid Sessions","free_sessions":"Free Sessions","cowriting":"Co-Writing"}},"ages":{"keys":[-1,10,20,30,40,50],"map":{"-1":"Any","10":"Teens","20":"20's","30":"30's","40":"40's","50":"50+"}}} + gon.musician_search_filter = @defaultFilter + + afterEach => + @server.restore() + + describe "displays empty search criteria", => + beforeEach => + @filterUI = new JK.MusicianSearchFilter() + @filterUI.init() + + it "displays validator widget", => + @server.respondWith("GET", "/api/search/musicians.json", [200, { "content-type": "application/json" }, JSON.stringify({sort_order: "latency", instruments: [], interests: "any", genres: [], concert_gigs: "-1", studio_sessions: "-1", skill_level: "-1", ages: [-1]})]) + sinon.spy() + @filterUI.afterShow(); + @server.respond() + expect($('select[name="sort_order"]').val()).toEqual(@defaultFilter.sort_order.keys[0]) + expect($('select[name="interests"]').val()).toEqual(@defaultFilter.interests.keys[0]) + expect($('select[name="skill_level"]').val()).toEqual("-1") + expect($('select[name="studio_sessions"]').val()).toEqual("-1") + expect($('select[name="concert_gigs"]').val()).toEqual("-1") + expect($('#search-filter-genres input[selected]').length).toEqual(0) + expect($('#search-filter-ages input[selected]').length).toEqual(0) + expect($('#search-filter-instruments input[selected]').length).toEqual(0) + expect($('#search-filter-instruments select.proficiency_selector[value=2]').length).toEqual(0) + + From f1808560ce110465547f8c0acde59e81cb70019b Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sun, 8 Mar 2015 22:57:19 -0400 Subject: [PATCH 055/195] VRFS-2701 added shells for all new edit screens --- .../accounts_profile_experience.js} | 0 web/app/assets/javascripts/accounts_profile_interests.js | 0 web/app/assets/javascripts/accounts_profile_samples.js | 0 web/app/assets/javascripts/dialog/genreSelector.js | 0 web/app/assets/javascripts/dialog/recordingSelector.js | 0 .../stylesheets/client/accountProfileExperience.css.scss | 0 .../assets/stylesheets/client/accountProfileInterests.css.scss | 0 .../assets/stylesheets/client/accountProfileSamples.css.scss | 0 web/app/assets/stylesheets/client/client.css | 3 +++ web/app/assets/stylesheets/dialogs/genreSelector.css.scss | 0 web/app/assets/stylesheets/dialogs/recordingSelector.css.scss | 0 web/app/views/clients/_account_profile_experience.html.slim | 0 web/app/views/clients/_account_profile_interests.html.slim | 0 web/app/views/clients/_account_profile_samples.html.slim | 0 web/app/views/clients/index.html.erb | 3 +++ web/app/views/dialogs/_genreSelector.html.slim | 0 web/app/views/dialogs/_recordingSelector.html.slim | 0 17 files changed, 6 insertions(+) rename web/app/assets/{stylesheets/client/genreSelector.css.scss => javascripts/accounts_profile_experience.js} (100%) create mode 100644 web/app/assets/javascripts/accounts_profile_interests.js create mode 100644 web/app/assets/javascripts/accounts_profile_samples.js create mode 100644 web/app/assets/javascripts/dialog/genreSelector.js create mode 100644 web/app/assets/javascripts/dialog/recordingSelector.js create mode 100644 web/app/assets/stylesheets/client/accountProfileExperience.css.scss create mode 100644 web/app/assets/stylesheets/client/accountProfileInterests.css.scss create mode 100644 web/app/assets/stylesheets/client/accountProfileSamples.css.scss create mode 100644 web/app/assets/stylesheets/dialogs/genreSelector.css.scss create mode 100644 web/app/assets/stylesheets/dialogs/recordingSelector.css.scss create mode 100644 web/app/views/clients/_account_profile_experience.html.slim create mode 100644 web/app/views/clients/_account_profile_interests.html.slim create mode 100644 web/app/views/clients/_account_profile_samples.html.slim create mode 100644 web/app/views/dialogs/_genreSelector.html.slim create mode 100644 web/app/views/dialogs/_recordingSelector.html.slim diff --git a/web/app/assets/stylesheets/client/genreSelector.css.scss b/web/app/assets/javascripts/accounts_profile_experience.js similarity index 100% rename from web/app/assets/stylesheets/client/genreSelector.css.scss rename to web/app/assets/javascripts/accounts_profile_experience.js diff --git a/web/app/assets/javascripts/accounts_profile_interests.js b/web/app/assets/javascripts/accounts_profile_interests.js new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/assets/javascripts/dialog/genreSelector.js b/web/app/assets/javascripts/dialog/genreSelector.js new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/assets/javascripts/dialog/recordingSelector.js b/web/app/assets/javascripts/dialog/recordingSelector.js new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/assets/stylesheets/client/accountProfileExperience.css.scss b/web/app/assets/stylesheets/client/accountProfileExperience.css.scss new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/assets/stylesheets/client/accountProfileInterests.css.scss b/web/app/assets/stylesheets/client/accountProfileInterests.css.scss new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index 05239eac4..2eab9998a 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -31,6 +31,9 @@ *= require ./findSession *= require ./session *= require ./account + *= require ./accountProfileExperience + *= require ./accountProfileInterests + *= require ./accountProfileSamples *= require ./search *= require ./ftue *= require ./jamServer diff --git a/web/app/assets/stylesheets/dialogs/genreSelector.css.scss b/web/app/assets/stylesheets/dialogs/genreSelector.css.scss new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/assets/stylesheets/dialogs/recordingSelector.css.scss b/web/app/assets/stylesheets/dialogs/recordingSelector.css.scss new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/views/clients/_account_profile_experience.html.slim b/web/app/views/clients/_account_profile_experience.html.slim new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/views/clients/_account_profile_interests.html.slim b/web/app/views/clients/_account_profile_interests.html.slim new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/views/clients/_account_profile_samples.html.slim b/web/app/views/clients/_account_profile_samples.html.slim new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 3f1ad8a1b..3aa1b4da4 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -49,6 +49,9 @@ <%= render "account_identity" %> <%= render "affiliate_report" %> <%= render "account_profile" %> +<%= render "account_profile_experience" %> +<%= render "account_profile_interests" %> +<%= render "account_profile_samples" %> <%= render "account_profile_avatar" %> <%= render "account_audio_profile" %> <%= render "account_sessions" %> diff --git a/web/app/views/dialogs/_genreSelector.html.slim b/web/app/views/dialogs/_genreSelector.html.slim new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/views/dialogs/_recordingSelector.html.slim b/web/app/views/dialogs/_recordingSelector.html.slim new file mode 100644 index 000000000..e69de29bb From d3f0bad60220441b92905fc3a3e8600feded430f Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 9 Mar 2015 05:15:15 +0000 Subject: [PATCH 056/195] VRFS-2795 filter tests --- .../musician_search_filter.js.coffee | 34 +++++++-------- web/app/controllers/spikes_controller.rb | 2 +- .../musician_search_filter_spec.js.coffee | 41 ++++++++++++++----- 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index a01c11886..b9925914a 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -4,7 +4,7 @@ context.JK ||= {}; context.JK.MusicianSearchFilter = class MusicianSearchFilter - constructor: (app) -> + constructor: () -> @rest = context.JK.Rest() @logger = context.JK.logger @userId = null @@ -61,8 +61,8 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter _populateSelectIdentifier: (identifier) => elem = $ '#musician-search-filter-builder select[name='+identifier+']' - struct = gon.musician_search_filter[identifier]['map'] - keys = gon.musician_search_filter[identifier]['keys'] + struct = gon.musician_search_meta[identifier]['map'] + keys = gon.musician_search_meta[identifier]['keys'] this._populateSelectWithKeys(struct, @searchFilter[identifier], keys, elem) _populateSelectWithInt: (sourceStruct, selection, element) => @@ -91,8 +91,8 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter populateAges: () => $('#search-filter-ages').empty() - ages_map = gon.musician_search_filter['ages']['map'] - $.each gon.musician_search_filter['ages']['keys'], (index, key) => + ages_map = gon.musician_search_meta['ages']['map'] + $.each gon.musician_search_meta['ages']['keys'], (index, key) => ageTemplate = $('#template-search-filter-setup-ages').html() selected = '' ageLabel = ages_map[key] @@ -109,6 +109,8 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter checked: selected) $('#search-filter-ages').append ageHtml + gotGenres: (genres) => + populateGenres: () => $('#search-filter-genres').empty() @rest.getGenres().done (genres) => @@ -118,15 +120,15 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter if 0 < @searchFilter.genres.length genreMatch = $.grep(@searchFilter.genres, (n, i) -> n == genre.id - ) - if genreMatch.length > 0 - selected = 'checked' - genreHtml = context.JK.fillTemplate(genreTemplate, - id: genre.id - description: genre.description - checked: selected) - $('#search-filter-genres').append genreHtml - + ) + if genreMatch.length > 0 + selected = 'checked' + genreHtml = context.JK.fillTemplate(genreTemplate, + id: genre.id + description: genre.description + checked: selected) + $('#search-filter-genres').append genreHtml + populateInstruments: () => $('#search-filter-instruments').empty() @rest.getInstruments().done (instruments) => @@ -172,10 +174,10 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter vals performSearch: () => - $.each gon.musician_search_filter.filter_keys.single, (index, key) => + $.each gon.musician_search_meta.filter_keys.single, (index, key) => @searchFilter[key] = this._selectedValue(key) - $.each gon.musician_search_filter.filter_keys.multi, (index, key) => + $.each gon.musician_search_meta.filter_keys.multi, (index, key) => @searchFilter[key] = this._selectedMultiValue(key) @rest.postMusicianSearchFilter { filter: JSON.stringify(@searchFilter), page: 1 } diff --git a/web/app/controllers/spikes_controller.rb b/web/app/controllers/spikes_controller.rb index 4d27974de..52c282084 100644 --- a/web/app/controllers/spikes_controller.rb +++ b/web/app/controllers/spikes_controller.rb @@ -62,7 +62,7 @@ class SpikesController < ApplicationController end def musician_search_filter - gon.musician_search_filter = MusicianSearch::SEARCH_FILTER_CONFIG + gon.musician_search_meta = MusicianSearch::SEARCH_FILTER_CONFIG render :layout => 'web' end diff --git a/web/spec/javascripts/musician_search_filter_spec.js.coffee b/web/spec/javascripts/musician_search_filter_spec.js.coffee index 5b2708981..1fbddb452 100644 --- a/web/spec/javascripts/musician_search_filter_spec.js.coffee +++ b/web/spec/javascripts/musician_search_filter_spec.js.coffee @@ -2,29 +2,36 @@ describe "MusicianSearchFilter", -> fixture.preload("musicianSearch.html") beforeEach => - @server = sinon.fakeServer.create(); + @filterServer = sinon.fakeServer.create(); window.jamClient = sinon.stub() @fixtures = fixture.load("musicianSearch.html", true) window.gon = {} window.gon.isNativeClient = true - @defaultFilter = {"per_page":10,"filter_keys":{"keys":["sort_order","instruments","interests","genres","concert_gigs","studio_sessions","skill_level","ages"],"multi":["instruments","genres","ages"],"single":["sort_order","interests","concert_gigs","studio_sessions","skill_level"]},"sort_order":{"keys":["latency","distance"],"map":{"latency":"Latency to Me","distance":"Distance to Me"}},"interests":{"keys":["any","virtual_band","traditional_band","paid_sessions","free_sessions","cowriting"],"map":{"any":"Any","virtual_band":"Virtual Band","traditional_band":"Traditional Band","paid_sessions":"Paid Sessions","free_sessions":"Free Sessions","cowriting":"Co-Writing"}},"ages":{"keys":[-1,10,20,30,40,50],"map":{"-1":"Any","10":"Teens","20":"20's","30":"30's","40":"40's","50":"50+"}}} - gon.musician_search_filter = @defaultFilter + @searchMeta = {"per_page":10,"filter_keys":{"keys":["sort_order","instruments","interests","genres","concert_gigs","studio_sessions","skill_level","ages"],"multi":["instruments","genres","ages"],"single":["sort_order","interests","concert_gigs","studio_sessions","skill_level"]},"sort_order":{"keys":["latency","distance"],"map":{"latency":"Latency to Me","distance":"Distance to Me"}},"interests":{"keys":["any","virtual_band","traditional_band","paid_sessions","free_sessions","cowriting"],"map":{"any":"Any","virtual_band":"Virtual Band","traditional_band":"Traditional Band","paid_sessions":"Paid Sessions","free_sessions":"Free Sessions","cowriting":"Co-Writing"}},"ages":{"keys":[-1,10,20,30,40,50],"map":{"-1":"Any","10":"Teens","20":"20's","30":"30's","40":"40's","50":"50+"}}} + window.gon.musician_search_meta = @searchMeta + @defaultFilter = {sort_order: "latency", instruments: [], interests: "any", genres: [], concert_gigs: "-1", studio_sessions: "-1", skill_level: "-1", ages: [-1]} + @genres = [{"description":"African","id":"african"},{"description":"Alternative Rock","id":"alternative rock"},{"description":"Ambient","id":"ambient"},{"description":"Americana","id":"americana"},{"description":"Asian","id":"asian"},{"description":"Avante-Garde","id":"avante-garde"},{"description":"Bluegrass","id":"bluegrass"},{"description":"Blues","id":"blues"},{"description":"Breakbeat","id":"breakbeat"},{"description":"Classical","id":"classical"},{"description":"Country","id":"country"},{"description":"Downtempo","id":"downtempo"},{"description":"Drum & Bass","id":"drum & bass"},{"description":"Electronic","id":"electronic"},{"description":"Folk","id":"folk"},{"description":"Hip Hop","id":"hip hop"},{"description":"House","id":"house"},{"description":"Industrial","id":"industrial"},{"description":"Jazz","id":"jazz"},{"description":"Latin","id":"latin"},{"description":"Metal","id":"metal"},{"description":"Other","id":"other"},{"description":"Pop","id":"pop"},{"description":"Psychedelic","id":"psychedelic"},{"description":"Punk","id":"punk"},{"description":"R&B","id":"r&b"},{"description":"Reggae","id":"reggae"},{"description":"Religious","id":"religious"},{"description":"Rock","id":"rock"},{"description":"Ska","id":"ska"},{"description":"Techno","id":"techno"},{"description":"Trance","id":"trance"}] + @instruments = [{"id":"acoustic guitar","description":"Acoustic Guitar","popularity":3},{"id":"bass guitar","description":"Bass Guitar","popularity":3},{"id":"computer","description":"Computer","popularity":3},{"id":"drums","description":"Drums","popularity":3},{"id":"electric guitar","description":"Electric Guitar","popularity":3},{"id":"keyboard","description":"Keyboard","popularity":3},{"id":"voice","description":"Voice","popularity":3},{"id":"accordion","description":"Accordion","popularity":2},{"id":"banjo","description":"Banjo","popularity":2},{"id":"clarinet","description":"Clarinet","popularity":2},{"id":"flute","description":"Flute","popularity":2},{"id":"harmonica","description":"Harmonica","popularity":2},{"id":"piano","description":"Piano","popularity":2},{"id":"saxophone","description":"Saxophone","popularity":2},{"id":"trombone","description":"Trombone","popularity":2},{"id":"trumpet","description":"Trumpet","popularity":2},{"id":"upright bass","description":"Upright Bass","popularity":2},{"id":"violin","description":"Violin","popularity":2},{"id":"cello","description":"Cello","popularity":1},{"id":"euphonium","description":"Euphonium","popularity":1},{"id":"french horn","description":"French Horn","popularity":1},{"id":"mandolin","description":"Mandolin","popularity":1},{"id":"oboe","description":"Oboe","popularity":1},{"id":"other","description":"Other","popularity":1},{"id":"tuba","description":"Tuba","popularity":1},{"id":"ukulele","description":"Ukulele","popularity":1},{"id":"viola","description":"Viola","popularity":1}] afterEach => - @server.restore() + @filterServer.restore() describe "displays empty search criteria", => beforeEach => @filterUI = new JK.MusicianSearchFilter() @filterUI.init() - it "displays validator widget", => - @server.respondWith("GET", "/api/search/musicians.json", [200, { "content-type": "application/json" }, JSON.stringify({sort_order: "latency", instruments: [], interests: "any", genres: [], concert_gigs: "-1", studio_sessions: "-1", skill_level: "-1", ages: [-1]})]) - sinon.spy() + it "displays validator widget for default filter", => + @filterServer.respondWith("GET", "/api/search/musicians.json", [200, { "content-type": "application/json" }, JSON.stringify(@defaultFilter)]) + @filterServer.respondWith("GET", "/api/genres.json", [200, { "content-type": "application/json" }, JSON.stringify(@genres)]) + @filterServer.respondWith("GET", "/api/getInstruments.json", [200, { "content-type": "application/json" }, JSON.stringify(@instruments)]) + @filterUI.afterShow(); - @server.respond() - expect($('select[name="sort_order"]').val()).toEqual(@defaultFilter.sort_order.keys[0]) - expect($('select[name="interests"]').val()).toEqual(@defaultFilter.interests.keys[0]) + @filterServer.respond() + expect($('#search-filter-genres input').length).toEqual(@genres.length) + expect($('#search-filter-instruments input').length).toEqual(@instruments.length) + expect($('select[name="sort_order"]').val()).toEqual(@searchMeta.sort_order.keys[0]) + expect($('select[name="interests"]').val()).toEqual(@searchMeta.interests.keys[0]) expect($('select[name="skill_level"]').val()).toEqual("-1") expect($('select[name="studio_sessions"]').val()).toEqual("-1") expect($('select[name="concert_gigs"]').val()).toEqual("-1") @@ -33,4 +40,18 @@ describe "MusicianSearchFilter", -> expect($('#search-filter-instruments input[selected]').length).toEqual(0) expect($('#search-filter-instruments select.proficiency_selector[value=2]').length).toEqual(0) + xit "displays validator widget for edited filter", => + @defaultFilter = {sort_order: @searchMeta.sort_order.keys[1], instruments: [], interests: @searchMeta.interests.keys[1], genres: ['ambient','asian'], concert_gigs: "1", studio_sessions: "1", skill_level: "1", ages: [-1]} + @filterServer.respondWith("GET", "/api/search/musicians.json", [200, { "content-type": "application/json" }, JSON.stringify(@defaultFilter)]) + sinon.spy() + @filterServer.respondWith("GET", "/api/genres.json", [200, { "content-type": "application/json" }, JSON.stringify(@genres)]) + sinon.spy() + @filterUI.afterShow(); + @filterServer.respond() + expect($('select[name="sort_order"]').val()).toEqual(@searchMeta.sort_order.keys[1]) + expect($('select[name="interests"]').val()).toEqual(@searchMeta.interests.keys[1]) + expect($('select[name="skill_level"]').val()).toEqual(@defaultFilter.skill_level) + expect($('select[name="studio_sessions"]').val()).toEqual(@defaultFilter.studio_sessions) + expect($('select[name="concert_gigs"]').val()).toEqual(@defaultFilter.concert_gigs) + expect($('#search-filter-genres input[checked]').length).toEqual(@defaultFilter.genres.length) From f29d370f2bd4bf697638960123e39ebf48ff7686 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 9 Mar 2015 01:36:49 -0400 Subject: [PATCH 057/195] VRFS-2701 wip edit musical experience --- .../assets/javascripts/accounts_profile.js | 12 +- .../accounts_profile_experience.js | 208 ++++++++++++++++++ web/app/assets/stylesheets/client/client.css | 1 - .../views/clients/_account_profile.html.erb | 2 +- .../clients/_account_profile_avatar.html.erb | 7 - .../_account_profile_experience.html.erb | 77 +++++++ ...im => _account_profile_interests.html.erb} | 0 ...slim => _account_profile_samples.html.erb} | 0 web/app/views/clients/index.html.erb | 9 + .../_genreSelector.html.erb} | 0 ....html.slim => _recordingSelector.html.erb} | 0 .../dialogs/_recordingSelector.html.slim | 0 12 files changed, 298 insertions(+), 18 deletions(-) create mode 100644 web/app/views/clients/_account_profile_experience.html.erb rename web/app/views/clients/{_account_profile_experience.html.slim => _account_profile_interests.html.erb} (100%) rename web/app/views/clients/{_account_profile_interests.html.slim => _account_profile_samples.html.erb} (100%) rename web/app/views/{clients/_account_profile_samples.html.slim => dialogs/_genreSelector.html.erb} (100%) rename web/app/views/dialogs/{_genreSelector.html.slim => _recordingSelector.html.erb} (100%) delete mode 100644 web/app/views/dialogs/_recordingSelector.html.slim diff --git a/web/app/assets/javascripts/accounts_profile.js b/web/app/assets/javascripts/accounts_profile.js index bdba3e8b9..ca979abc5 100644 --- a/web/app/assets/javascripts/accounts_profile.js +++ b/web/app/assets/javascripts/accounts_profile.js @@ -36,7 +36,7 @@ } function populateAccountProfile(userDetail) { - var template = context.JK.fillTemplate($('#template-account-profile').html(), { + var template = context.JK.fillTemplate($('#template-account-profile-basics').html(), { country: userDetail.country, region: userDetail.state, city: userDetail.city, @@ -49,7 +49,7 @@ subscribe_email: userDetail.subscribe_email ? "checked=checked" : "" }); - var content_root = $('#account-profile-content-scroller') + var content_root = $('#account-profile-content-scroller'); content_root.html(template); $biography = $screen.find('#biography'); @@ -271,14 +271,8 @@ } function postUpdateProfileSuccess(response) { - app.notify( - { title: "Profile Changed", - text: "You have updated your profile successfully." - }, - null, - true); - $document.triggerHandler(EVENTS.USER_UPDATED, response); + context.location = "/client#/account/profile/experience"; } function postUpdateProfileFailure(xhr, textStatus, errorMessage) { diff --git a/web/app/assets/javascripts/accounts_profile_experience.js b/web/app/assets/javascripts/accounts_profile_experience.js index e69de29bb..3d5b1fc6a 100644 --- a/web/app/assets/javascripts/accounts_profile_experience.js +++ b/web/app/assets/javascripts/accounts_profile_experience.js @@ -0,0 +1,208 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.AccountProfileExperience = function(app) { + var $document = $(document); + var logger = context.JK.logger; + var EVENTS = context.JK.EVENTS; + var api = context.JK.Rest(); + var userId; + var user = {}; + var $screen = $('#account-profile-experience'); + var $scroller = $screen.find('#account-profile-content-scroller'); + var $instrumentSelector = null; + var $userGenres = null; + + function beforeShow(data) { + userId = data.id; + } + + function afterShow(data) { + resetForm(); + renderExperience(); + } + + function resetForm() { + $scroller.find('form .error-text').remove(); + $scroller.find('form .error').removeClass("error"); + } + + function populateAccountProfile(userDetail, instruments) { + var template = context.JK.fillTemplate($('#template-account-profile-experience').html(), { + user_instruments: userDetail.instruments + }); + + $scroller.html(template); + + $instrumentSelector = $screen.find('.instrument_selector'); + $userGenres = $screen.find('#user-genres'); + + loadGenres(userDetail.genres); + + $.each(instruments, function(index, instrument) { + var template = context.JK.fillTemplate($('#account-profile-instrument').html(), { + checked : isUserInstrument(instrument, userDetail.instruments) ? "checked=\"checked\"" :"", + description : instrument.description, + id : instrument.id + }); + $instrumentSelector.append(template) + }); + + // and fill in the proficiency for the instruments that the user can play + if(userDetail.instruments) { + $.each(userDetail.instruments, function(index, userInstrument) { + $('tr[data-instrument-id="' + userInstrument.instrument_id + '"] select.proficiency_selector', $scroller).val(userInstrument.proficiency_level); + }); + } + + context.JK.dropdown($('select', $scroller)); + } + + function isUserInstrument(instrument, userInstruments) { + var isUserInstrument = false; + if(!userInstruments) return false; + + $.each(userInstruments, function(index, userInstrument) { + if(instrument.id == userInstrument.instrument_id) { + isUserInstrument = true; + return false; + } + }); + return isUserInstrument; + } + + function loadGenres(selectedGenres) { + $userGenres.empty(); + + rest.getGenres().done(function (genres) { + $.each(genres, function (index, genre) { + var genreTemplate = $('#template-user-setup-genres').html(); + var selected = ''; + if (selectedGenres) { + var genreMatch = $.grep(selectedGenres, function (n, i) { + return n.id === genre.id; + }); + if (genreMatch.length > 0) { + selected = "checked"; + } + } + var genreHtml = context.JK.fillTemplate(genreTemplate, { + id: genre.id, + description: genre.description, + checked: selected + }); + $userGenres.append(genreHtml); + }); + }); + } + + function resetGenres() { + $('input[type=checkbox]:checked', $userGenres).each(function (i) { + $(this).removeAttr("checked"); + }); + var $tdGenres = $("#tdBandGenres"); + } + + function getSelectedGenres() { + var genres = []; + $('input[type=checkbox]:checked', $userGenres).each(function (i) { + var genre = $(this).val(); + genres.push(genre); + }); + return genres; + } + + /****************** MAIN PORTION OF SCREEN *****************/ + // events for main screen + function events() { + $screen.find('#account-edit-profile-cancel').on('click', function(evt) { evt.stopPropagation(); navigateTo('/client#/account'); return false; } ); + $screen.find('#account-edit-profile-cancel').on('click', function(evt) { evt.stopPropagation(); navigateTo('/client#/account/profile'); return false; } ); + $screen.find('#account-edit-profile-submit').on('click', function(evt) { evt.stopPropagation(); handleUpdateProfile(); return false; } ); + } + + function renderExperience() { + $.when(api.getUserDetail(), api.getInstruments()) + .done(function(userDetailResponse, instrumentsResponse) { + var userDetail = userDetailResponse[0]; + populateAccountProfile(userDetail, instrumentsResponse[0]); + }); + + context.JK.dropdown($('select')); + } + + function navigateTo(targetLocation) { + resetForm(); + window.location = targetLocation; + } + + function handleUpdateProfile() { + resetForm(); + + var instruments = getInstrumentsValue(); + var genres = getSelectedGenres(); + + api.updateUser({ + instruments: instruments, + genres: genres + }) + .done(postUpdateProfileSuccess) + .fail(postUpdateProfileFailure); + } + + function postUpdateProfileSuccess(response) { + $document.triggerHandler(EVENTS.USER_UPDATED, response); + context.location = "/client#/account/profile/interests"; + } + + function postUpdateProfileFailure(xhr, textStatus, errorMessage) { + + var errors = JSON.parse(xhr.responseText) + + if(xhr.status == 422) { + var instruments = context.JK.format_errors("musician_instruments", errors); + + if(instruments != null) { + instrumentSelector.closest('div.field').addClass('error').append(instruments); + } + } + else { + app.ajaxError(xhr, textStatus, errorMessage) + } + } + + function getInstrumentsValue() { + var instruments = []; + $('input[type=checkbox]:checked', $instrumentSelector).each(function(i) { + + var instrumentElement = $(this).closest('tr'); + // traverse up to common parent of this instrument, and pick out proficiency selector + var proficiency = $('select.proficiency_selector', instrumentElement).val(); + + instruments.push({ + instrument_id: instrumentElement.attr('data-instrument-id'), + proficiency_level: proficiency, + priority : i + }); + }); + + return instruments; + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + app.bindScreen('account/profile/experience', screenBindings); + events(); + } + + this.initialize = initialize; + this.beforeShow = beforeShow; + this.afterShow = afterShow; + return this; + }; + +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index 2eab9998a..c953408ec 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -56,7 +56,6 @@ *= require ./jamtrack *= require ./shoppingCart *= require ./checkout - *= require ./genreSelector *= require ./sessionList *= require ./searchResults *= require ./clientUpdate diff --git a/web/app/views/clients/_account_profile.html.erb b/web/app/views/clients/_account_profile.html.erb index 7ea889540..08312237b 100644 --- a/web/app/views/clients/_account_profile.html.erb +++ b/web/app/views/clients/_account_profile.html.erb @@ -21,7 +21,7 @@
- - - diff --git a/web/app/views/clients/_account_profile_experience.html.erb b/web/app/views/clients/_account_profile_experience.html.erb new file mode 100644 index 000000000..d6fee4fd9 --- /dev/null +++ b/web/app/views/clients/_account_profile_experience.html.erb @@ -0,0 +1,77 @@ + +
+ +
+ +
+ <%= image_tag "content/icon_account.png", {:width => 27, :height => 20} %> +
+ +

my account

+ <%= render "screen_navigation" %> +
+ + + +
+ +
+ +
+ + + + + + diff --git a/web/app/views/clients/_account_profile_experience.html.slim b/web/app/views/clients/_account_profile_interests.html.erb similarity index 100% rename from web/app/views/clients/_account_profile_experience.html.slim rename to web/app/views/clients/_account_profile_interests.html.erb diff --git a/web/app/views/clients/_account_profile_interests.html.slim b/web/app/views/clients/_account_profile_samples.html.erb similarity index 100% rename from web/app/views/clients/_account_profile_interests.html.slim rename to web/app/views/clients/_account_profile_samples.html.erb diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 3aa1b4da4..77a0a0f0a 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -211,6 +211,15 @@ var accountProfileAvatarScreen = new JK.AccountProfileAvatarScreen(JK.app); accountProfileAvatarScreen.initialize(JK.UserDropdown); + var accountProfileExperience = new JK.AccountProfileExperience(JK.app); + accountProfileExperience.initialize(); + + // var accountProfileInterests = new JK.AccountProfileInterests(JK.app); + // accountProfileInterests.initialize(); + + // var accountProfileSamples = new JK.AccountProfileSamples(JK.app); + // accountProfileSamples.initialize(); + var accountAudioProfile = new JK.AccountAudioProfile(JK.app); accountAudioProfile.initialize(); diff --git a/web/app/views/clients/_account_profile_samples.html.slim b/web/app/views/dialogs/_genreSelector.html.erb similarity index 100% rename from web/app/views/clients/_account_profile_samples.html.slim rename to web/app/views/dialogs/_genreSelector.html.erb diff --git a/web/app/views/dialogs/_genreSelector.html.slim b/web/app/views/dialogs/_recordingSelector.html.erb similarity index 100% rename from web/app/views/dialogs/_genreSelector.html.slim rename to web/app/views/dialogs/_recordingSelector.html.erb diff --git a/web/app/views/dialogs/_recordingSelector.html.slim b/web/app/views/dialogs/_recordingSelector.html.slim deleted file mode 100644 index e69de29bb..000000000 From 33f10e31972fec065971059aac21d6c9a006b657 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Mon, 9 Mar 2015 06:13:56 +0000 Subject: [PATCH 058/195] VRFS-2795 fixing search specs --- ruby/lib/jam_ruby/models/musician_search.rb | 41 ++----------------- .../models/musician_search_model_spec.rb | 27 +++++------- 2 files changed, 14 insertions(+), 54 deletions(-) diff --git a/ruby/lib/jam_ruby/models/musician_search.rb b/ruby/lib/jam_ruby/models/musician_search.rb index a18fdd4d4..728915c6d 100644 --- a/ruby/lib/jam_ruby/models/musician_search.rb +++ b/ruby/lib/jam_ruby/models/musician_search.rb @@ -37,10 +37,6 @@ module JamRuby GIG_COUNTS[3] => 'More than 50', GIG_COUNTS[4] => 'More than 100' } - GIG_RANGES = [(GIG_COUNTS[1]...GIG_COUNTS[2]), - (GIG_COUNTS[2]...GIG_COUNTS[3]), - (GIG_COUNTS[3]...GIG_COUNTS[4]), - (GIG_COUNTS[4]...PG_SMALLINT_MAX)] STUDIO_COUNTS = [ANY_VAL_INT, 0, 1, 2, 3, 4] STUDIOS_LABELS = { @@ -50,10 +46,6 @@ module JamRuby STUDIO_COUNTS[3] => 'More than 50', STUDIO_COUNTS[4] => 'More than 100' } - STUDIO_RANGES = [(STUDIO_COUNTS[1]...STUDIO_COUNTS[2]), - (STUDIO_COUNTS[2]...STUDIO_COUNTS[3]), - (STUDIO_COUNTS[3]...STUDIO_COUNTS[4]), - (STUDIO_COUNTS[4]...PG_SMALLINT_MAX)] AGE_COUNTS = [ANY_VAL_INT, 10, 20, 30, 40, 50] AGES = { @@ -179,39 +171,14 @@ module JamRuby end def _studios(rel) - if 0 < (count = json[KEY_STUDIOS].to_i) - rel = rel.where('studio_session_count IS NOT NULL') - case count - when STUDIO_COUNTS[1] - rel = rel.where('studio_session_count >= ? AND studio_session_count < ?', - count, STUDIO_COUNTS[2]) - when STUDIO_COUNTS[2] - rel = rel.where('studio_session_count >= ? AND studio_session_count < ?', - count, STUDIO_COUNTS[3]) - when STUDIO_COUNTS[3] - rel = rel.where('studio_session_count >= ? AND studio_session_count < ?', - count, STUDIO_COUNTS[4]) - when STUDIO_COUNTS[4] - rel = rel.where('studio_session_count >= ?', count) - end - end + ss = json[KEY_STUDIOS].to_i + rel = rel.where('studio_session_count = ?', ss) if 0 <= ss rel end def _gigs(rel) - if 0 < (count = json[KEY_GIGS].to_i) - rel = rel.where('concert_count IS NOT NULL') - case count - when GIG_COUNTS[1] - rel = rel.where('concert_count >= ? AND concert_count < ?', count, GIG_COUNTS[2]) - when GIG_COUNTS[2] - rel = rel.where('concert_count >= ? AND concert_count < ?', count, GIG_COUNTS[3]) - when GIG_COUNTS[3] - rel = rel.where('concert_count >= ? AND concert_count < ?', count, GIG_COUNTS[4]) - when GIG_COUNTS[4] - rel = rel.where('concert_count >= ?', count) - end - end + gg = json[KEY_GIGS].to_i + rel = rel.where('concert_count = ?',gg) if 0 <= gg rel end diff --git a/ruby/spec/jam_ruby/models/musician_search_model_spec.rb b/ruby/spec/jam_ruby/models/musician_search_model_spec.rb index 85668c18f..e6655a123 100644 --- a/ruby/spec/jam_ruby/models/musician_search_model_spec.rb +++ b/ruby/spec/jam_ruby/models/musician_search_model_spec.rb @@ -63,7 +63,7 @@ describe 'Musician Search Model' do end it "skips filtering by ages" do - search.update_json_value(MusicianSearch::KEY_AGES, [0]) + search.update_json_value(MusicianSearch::KEY_AGES, [MusicianSearch::ANY_VAL_INT]) search.do_search.to_sql =~ /(birth_date)/ expect($1).to eq(nil) end @@ -72,10 +72,8 @@ describe 'Musician Search Model' do describe "filtering by gig" do before(:all) do User.delete_all - MusicianSearch::GIG_RANGES.each do |range| - user_types.each do |utype| - FactoryGirl.create(utype, :concert_count => rand(range)) - end + user_types.each do |utype| + FactoryGirl.create(utype, :concert_count => MusicianSearch::GIG_COUNTS[1]) end end @@ -85,20 +83,16 @@ describe 'Musician Search Model' do end it "filters by gig count" do - MusicianSearch::GIG_COUNTS[1..-1].each do |count| - search.update_json_value(MusicianSearch::KEY_GIGS, count) - expect(search.do_search.count).to eq(user_types.count) - end + search.update_json_value(MusicianSearch::KEY_GIGS, MusicianSearch::GIG_COUNTS[1]) + expect(search.do_search.count).to eq(user_types.count) end end describe "filtering by studio" do before(:all) do User.delete_all - MusicianSearch::STUDIO_RANGES.each do |range| - user_types.each do |utype| - FactoryGirl.create(utype, :studio_session_count => rand(range)) - end + user_types.each do |utype| + FactoryGirl.create(utype, :studio_session_count => MusicianSearch::STUDIO_COUNTS[1]) end end @@ -108,10 +102,8 @@ describe 'Musician Search Model' do end it "filters by studio count" do - MusicianSearch::STUDIO_COUNTS[1..-1].each do |count| - search.update_json_value(MusicianSearch::KEY_STUDIOS, count) - expect(search.do_search.count).to eq(user_types.count) - end + search.update_json_value(MusicianSearch::KEY_STUDIOS, MusicianSearch::STUDIO_COUNTS[1]) + expect(search.do_search.count).to eq(user_types.count) end end @@ -124,6 +116,7 @@ describe 'Musician Search Model' do end it "get expected number per skill" do + search.update_json_value(MusicianSearch::KEY_SKILL, MusicianSearch::SKILL_VALS[1]) expect(search.do_search.count).to eq(user_types.count) end end From f10bdc18f4e59488d9a08831f3a6d61a9f0715a5 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Tue, 10 Mar 2015 02:02:59 -0400 Subject: [PATCH 059/195] VRFS-2701 wip saving musical experience working --- .../accounts_profile_experience.js | 12 ++-- .../stylesheets/client/account.css.scss | 24 -------- .../client/accountProfileExperience.css.scss | 44 ++++++++++++++ .../stylesheets/client/profile.css.scss | 12 ---- .../_account_profile_experience.html.erb | 59 +++++++++++++++---- 5 files changed, 97 insertions(+), 54 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile_experience.js b/web/app/assets/javascripts/accounts_profile_experience.js index 3d5b1fc6a..aa7749d76 100644 --- a/web/app/assets/javascripts/accounts_profile_experience.js +++ b/web/app/assets/javascripts/accounts_profile_experience.js @@ -38,6 +38,7 @@ $instrumentSelector = $screen.find('.instrument_selector'); $userGenres = $screen.find('#user-genres'); + events(); loadGenres(userDetail.genres); @@ -114,11 +115,9 @@ return genres; } - /****************** MAIN PORTION OF SCREEN *****************/ - // events for main screen function events() { $screen.find('#account-edit-profile-cancel').on('click', function(evt) { evt.stopPropagation(); navigateTo('/client#/account'); return false; } ); - $screen.find('#account-edit-profile-cancel').on('click', function(evt) { evt.stopPropagation(); navigateTo('/client#/account/profile'); return false; } ); + $screen.find('#account-edit-profile-back').on('click', function(evt) { evt.stopPropagation(); navigateTo('/client#/account/profile'); return false; } ); $screen.find('#account-edit-profile-submit').on('click', function(evt) { evt.stopPropagation(); handleUpdateProfile(); return false; } ); } @@ -142,10 +141,14 @@ var instruments = getInstrumentsValue(); var genres = getSelectedGenres(); + var status = api.updateUser({ instruments: instruments, - genres: genres + genres: genres, + skill_level: $scroller.find('select[name=gender]').val(), + concert_count: $scroller.find('select[name=concert_count]').val(), + studio_session_count: $scroller.find('select[name=studio_session_count]').val(), }) .done(postUpdateProfileSuccess) .fail(postUpdateProfileFailure); @@ -196,7 +199,6 @@ 'afterShow': afterShow }; app.bindScreen('account/profile/experience', screenBindings); - events(); } this.initialize = initialize; diff --git a/web/app/assets/stylesheets/client/account.css.scss b/web/app/assets/stylesheets/client/account.css.scss index 445d07eec..69b8ef318 100644 --- a/web/app/assets/stylesheets/client/account.css.scss +++ b/web/app/assets/stylesheets/client/account.css.scss @@ -119,25 +119,6 @@ white-space: normal; } - .profile-instrumentlist { - background-color: #C5C5C5; - border: medium none; - box-shadow: 2px 2px 3px 0 #888888 inset; - color: #000; - font-size: 14px; - height: 100px; - overflow: auto; - width: 100%; - - .easydropdown-wrapper { - width:100%; - } - - select, .easydropdown { - @include flat_dropdown; - } - } - .account-sub-description { display: block; white-space: normal; @@ -199,11 +180,6 @@ } } - div.profile-instrumentlist table { - border-collapse: separate; - border-spacing: 6px; - } - .account-edit-email, .account-edit-password { width:35%; } diff --git a/web/app/assets/stylesheets/client/accountProfileExperience.css.scss b/web/app/assets/stylesheets/client/accountProfileExperience.css.scss index e69de29bb..1f21ad04a 100644 --- a/web/app/assets/stylesheets/client/accountProfileExperience.css.scss +++ b/web/app/assets/stylesheets/client/accountProfileExperience.css.scss @@ -0,0 +1,44 @@ +@import "client/common.css.scss"; + + +#account-profile-experience { + .genres { + width:100%; + height:200px; + background-color:#c5c5c5; + border:none; + -webkit-box-shadow: inset 2px 2px 3px 0px #888; + box-shadow: inset 2px 2px 3px 0px #888; + color:#000; + overflow:auto; + font-size:14px; + } + + .instruments { + background-color: #C5C5C5; + border: medium none; + box-shadow: 2px 2px 3px 0 #888888 inset; + color: #000; + font-size: 14px; + height: 200px; + overflow: auto; + width: 85%; + + .easydropdown-wrapper { + width:100%; + } + + select, .easydropdown { + @include flat_dropdown; + } + } + + div.instruments table { + border-collapse: separate; + border-spacing: 6px; + } + + div.actions { + margin-right: 100px; + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/profile.css.scss b/web/app/assets/stylesheets/client/profile.css.scss index 86d732695..94d057129 100644 --- a/web/app/assets/stylesheets/client/profile.css.scss +++ b/web/app/assets/stylesheets/client/profile.css.scss @@ -209,18 +209,6 @@ padding-right:5px; } -.user-setup-genres { - width:40%; - height:90px; - background-color:#c5c5c5; - border:none; - -webkit-box-shadow: inset 2px 2px 3px 0px #888; - box-shadow: inset 2px 2px 3px 0px #888; - color:#000; - overflow:auto; - font-size:14px; -} - .profile-band-list-result { width:100%; min-height:85px; diff --git a/web/app/views/clients/_account_profile_experience.html.erb b/web/app/views/clients/_account_profile_experience.html.erb index d6fee4fd9..d4091e602 100644 --- a/web/app/views/clients/_account_profile_experience.html.erb +++ b/web/app/views/clients/_account_profile_experience.html.erb @@ -26,34 +26,67 @@
-

edit profile: musical experience

-

-
+

edit profile: musical experience

-
+
-
+
What instruments do you play? -
+
-
- CANCEL   - BACK   - SAVE & NEXT -
-
+
-
+
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ +

+ +
+ CANCEL   + BACK   + SAVE & NEXT +
+ +
From 41626860a9f84ef43de7167abaed54bae01c9b8a Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 12 Mar 2015 04:39:20 +0000 Subject: [PATCH 060/195] VRFS-2795 replacing musician page with new search filter --- ruby/lib/jam_ruby/models/musician_search.rb | 2 +- .../musician_search_filter.js.coffee | 28 +++---- web/app/controllers/spikes_controller.rb | 2 +- web/app/helpers/client_helper.rb | 3 +- .../clients/_musician_search_filter.html.slim | 14 ++-- web/app/views/clients/_musicians.html.erb | 83 ------------------- web/app/views/clients/_musicians.html.slim | 60 ++++++++++++++ web/app/views/clients/index.html.erb | 6 +- 8 files changed, 90 insertions(+), 108 deletions(-) delete mode 100644 web/app/views/clients/_musicians.html.erb create mode 100644 web/app/views/clients/_musicians.html.slim diff --git a/ruby/lib/jam_ruby/models/musician_search.rb b/ruby/lib/jam_ruby/models/musician_search.rb index 728915c6d..395728644 100644 --- a/ruby/lib/jam_ruby/models/musician_search.rb +++ b/ruby/lib/jam_ruby/models/musician_search.rb @@ -87,7 +87,7 @@ module JamRuby MULTI_VALUE_KEYS = JSON_SCHEMA.collect { |kk,vv| vv.is_a?(Array) ? kk : nil }.compact SINGLE_VALUE_KEYS = JSON_SCHEMA.keys - MULTI_VALUE_KEYS - SEARCH_FILTER_CONFIG = { + SEARCH_FILTER_META = { per_page: PER_PAGE, filter_keys: { keys: JSON_SCHEMA_KEYS, diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index b9925914a..4410b2548 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -11,8 +11,9 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter @searchFilter = null @profileUtils = context.JK.ProfileUtils - init: () => + init: (app) => @screenBindings = { 'beforeShow': this.beforeShow, 'afterShow': this.afterShow } + app.bindScreen('musicians', @screenBindings); srchbtn = $ '#btn-perform-musician-search' srchbtn.on 'click', => this.performSearch() @@ -109,25 +110,25 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter checked: selected) $('#search-filter-ages').append ageHtml - gotGenres: (genres) => - populateGenres: () => $('#search-filter-genres').empty() @rest.getGenres().done (genres) => + genreTemplate = $('#template-search-filter-setup-genres').html() + selected = '' $.each genres, (index, genre) => - genreTemplate = $('#template-search-filter-setup-genres').html() - selected = '' - if 0 < @searchFilter.genres.length + if 0 < @searchFilter.genres.length genreMatch = $.grep(@searchFilter.genres, (n, i) -> n == genre.id - ) + ) + else + genreMatch = [] if genreMatch.length > 0 selected = 'checked' - genreHtml = context.JK.fillTemplate(genreTemplate, - id: genre.id - description: genre.description - checked: selected) - $('#search-filter-genres').append genreHtml + genreHtml = context.JK.fillTemplate(genreTemplate, + id: genre.id + description: genre.description + checked: selected) + $('#search-filter-genres').append genreHtml populateInstruments: () => $('#search-filter-instruments').empty() @@ -142,8 +143,7 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter proficiency = inst.proficiency_level if yn yn ) - if instMatch.length > 0 - selected = 'checked' + selected = instMatch.length > 0 ? 'checked' : '' instrumentHtml = context.JK.fillTemplate(instrumentTemplate, id: instrument.id description: instrument.description diff --git a/web/app/controllers/spikes_controller.rb b/web/app/controllers/spikes_controller.rb index 52c282084..1602b6a25 100644 --- a/web/app/controllers/spikes_controller.rb +++ b/web/app/controllers/spikes_controller.rb @@ -62,7 +62,7 @@ class SpikesController < ApplicationController end def musician_search_filter - gon.musician_search_meta = MusicianSearch::SEARCH_FILTER_CONFIG + # gon.musician_search_meta = MusicianSearch::SEARCH_FILTER_META render :layout => 'web' end diff --git a/web/app/helpers/client_helper.rb b/web/app/helpers/client_helper.rb index 6f4d0a489..c39e42878 100644 --- a/web/app/helpers/client_helper.rb +++ b/web/app/helpers/client_helper.rb @@ -48,6 +48,7 @@ module ClientHelper gon.ftue_network_test_duration = Rails.application.config.ftue_network_test_duration gon.ftue_network_test_max_clients = Rails.application.config.ftue_network_test_max_clients gon.ftue_maximum_gear_latency = Rails.application.config.ftue_maximum_gear_latency + gon.musician_search_meta = MusicianSearch::SEARCH_FILTER_META # is this the native client or browser? @nativeClient = is_native_client? @@ -58,4 +59,4 @@ module ClientHelper gon.use_cached_session_scores = Rails.application.config.use_cached_session_scores gon.allow_both_find_algos = Rails.application.config.allow_both_find_algos end -end \ No newline at end of file +end diff --git a/web/app/views/clients/_musician_search_filter.html.slim b/web/app/views/clients/_musician_search_filter.html.slim index 52f31775b..b02a4da41 100644 --- a/web/app/views/clients/_musician_search_filter.html.slim +++ b/web/app/views/clients/_musician_search_filter.html.slim @@ -1,6 +1,8 @@ -div#musician-search-filter-builder - a#btn-perform-musician-search.button-grey href="#" SEARCH - div#musician-search-filter-builder-form +.content-body-scroller + div#musician-search-filter-builder.content-wrapper + h2 search musicians + a#btn-perform-musician-search.button-grey href="#" SEARCH + div#musician-search-filter-builder-form script#template-musician-search-filter type="text/template" .field @@ -30,17 +32,17 @@ script#template-musician-search-filter type="text/template" .field label for="search-filter-ages" Ages: - .search-filter-setup-ages + .search-filter-setup-ages.w90.band-setup-genres table#search-filter-ages cellpadding="10" cellspacing="6" width="100%" .field label for="search-filter-genres" Genres: - .search-filter-setup-genres + .search-filter-setup-genres.w90.band-setup-genres table#search-filter-genres cellpadding="10" cellspacing="6" width="100%" .field label for="search-filter-instruments" Instruments: - .search-filter-setup-instruments + .search-filter-setup-instruments.w90.band-setup-genres table#search-filter-instruments cellpadding="10" cellspacing="6" width="100%" script#template-search-filter-setup-instrument type="text/template" diff --git a/web/app/views/clients/_musicians.html.erb b/web/app/views/clients/_musicians.html.erb deleted file mode 100644 index c31f8648e..000000000 --- a/web/app/views/clients/_musicians.html.erb +++ /dev/null @@ -1,83 +0,0 @@ - -<%= content_tag(:div, :layout => 'screen', 'layout-id' => 'musicians', :class => "screen secondary", id: 'musicians-screen') do -%> - <%= content_tag(:div, :class => :content) do -%> - <%= content_tag(:div, :class => 'content-head') do -%> - <%= content_tag(:div, image_tag("content/icon_musicians.png", {:height => 19, :width => 19}), :class => 'content-icon') %> - <%= content_tag(:h1, 'musicians') %> - <%= render "screen_navigation" %> - <% end -%> - <%= content_tag(:div, :class => 'content-body') do -%> - <%= form_tag('', {:id => 'find-musician-form', :class => 'inner-content'}) do -%> - <%= render(:partial => "web_filter", :locals => {:search_type => Search::PARAM_MUSICIAN}) %> - <%= content_tag(:div, :class => 'filter-body') do %> - <%= content_tag(:div, :class => 'content-body-scroller') do -%> - <%= content_tag(:div, :class => 'content-wrapper musician-wrapper') do -%> - <%= content_tag(:div, '', :id => 'musician-filter-results', :class => 'filter-results') %> -
Fetching more results... -
-
- <%= content_tag(:div, 'No more results.', :class => 'end-of-list', :id => 'end-of-musician-list') %> - <% end -%> - <% end -%> - <% end -%> - <% end -%> - <% end -%> - <% end -%> -<% end -%> - - - - - \ No newline at end of file diff --git a/web/app/views/clients/_musicians.html.slim b/web/app/views/clients/_musicians.html.slim new file mode 100644 index 000000000..5356da76e --- /dev/null +++ b/web/app/views/clients/_musicians.html.slim @@ -0,0 +1,60 @@ +#musicians-screen.screen.secondary layout="screen" layout-id="musicians" + .content + .content-head + .content-icon + img alt="Icon_musicians" height="19" src="/assets/content/icon_musicians.png" width="19" / + h1 musicians + = render "screen_navigation" + .content-body + = render "clients/musician_search_filter" + +/! Session Row Template +script#template-find-musician-row type="text/template" + .profile-band-list-result.musician-list-result data-musician-id="{musician_id}" + .f11 data-hint="container" + .left.musician-avatar + /! avatar + .avatar-small + img src="{avatar_url}" / + .left.musician-info + .first-row data-hint="top-row" + .musician-profile + .result-name musician_name + .result-location musician_location + #result_instruments.instruments.nowrap.mt10 instruments + .musician-stats + span.friend-count + | {friend_count} + img align="absmiddle" alt="friends" height="12" src="../assets/content/icon_friend.png" style="margin-right:4px;" width="14" / + span.follower-count + | {follow_count} + img align="absmiddle" alt="followers" height="12" src="../assets/content/icon_followers.png" style="margin-right:4px;" width="22" / + span.recording-count + | {recording_count} + img align="absmiddle" alt="recordings" height="13" src="../assets/content/icon_recordings.png" style="margin-right:4px;" width="12" / + span.session-count + | {session_count} + img align="absmiddle" alt="sessions" height="12" src="../assets/content/icon_session_tiny.png" style="margin-right:4px;" width="12" / + br clear="both" / + .left.musician-latency + .latency-help + | Your latency + br>/ + | to {musician_first_name} is: + .latency-holder + | {latency_badge} + br clear="both" / + .button-row data-hint="button-row" + .biography biography + .result-list-button-wrapper data-musician-id="{musician_id}" + | {musician_action_template} + br clear="both" / + + +script#template-musician-action-btns type="text/template" + a.button-orange.smallbutton href="{profile_url}" PROFILE + - if current_user && current_user.musician? + a.smallbutton.search-m-friend class="{friend_class}" href="#" {friend_caption} + a.smallbutton.search-m-follow class="{follow_class}" href="#" {follow_caption} + a.smallbutton.search-m-message class="{message_class}" href="#" {message_caption" + .clearall diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 77a0a0f0a..6072e067e 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -267,8 +267,10 @@ var OrderScreen = new JK.OrderScreen(JK.app); OrderScreen.initialize(); - var findMusicianScreen = new JK.FindMusicianScreen(JK.app); - findMusicianScreen.initialize(JK.TextMessageDialogInstance); + var findMusicianScreen = new JK.MusicianSearchFilter(); + findMusicianScreen.init(JK.app); + // var findMusicianScreen = new JK.FindMusicianScreen(JK.app); + //findMusicianScreen.initialize(JK.TextMessageDialogInstance); var findBandScreen = new JK.FindBandScreen(JK.app); findBandScreen.initialize(); From 4e456bce41c1f1c0fcab07c94d2c5b2b69e93c46 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Fri, 13 Mar 2015 02:26:09 -0400 Subject: [PATCH 061/195] VRFS-2701 wip current interests editing --- .../accounts_profile_experience.js | 6 +- .../javascripts/accounts_profile_interests.js | 116 +++++++++ .../client/accountProfileInterests.css.scss | 43 ++++ .../_account_profile_experience.html.erb | 7 - .../_account_profile_interests.html.erb | 222 ++++++++++++++++++ web/app/views/clients/index.html.erb | 4 +- 6 files changed, 386 insertions(+), 12 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile_experience.js b/web/app/assets/javascripts/accounts_profile_experience.js index a85888b9f..826cf09aa 100644 --- a/web/app/assets/javascripts/accounts_profile_experience.js +++ b/web/app/assets/javascripts/accounts_profile_experience.js @@ -48,7 +48,7 @@ description : instrument.description, id : instrument.id }); - $instrumentSelector.append(template) + $instrumentSelector.append(template); }); // and fill in the proficiency for the instruments that the user can play @@ -137,7 +137,7 @@ function navigateTo(targetLocation) { resetForm(); - window.location = targetLocation; + context.location = targetLocation; } function handleUpdateProfile() { @@ -152,7 +152,7 @@ genres: genres, skill_level: $scroller.find('select[name=skill_level]').val(), concert_count: $scroller.find('select[name=concert_count]').val(), - studio_session_count: $scroller.find('select[name=studio_session_count]').val(), + studio_session_count: $scroller.find('select[name=studio_session_count]').val() }) .done(postUpdateProfileSuccess) .fail(postUpdateProfileFailure); diff --git a/web/app/assets/javascripts/accounts_profile_interests.js b/web/app/assets/javascripts/accounts_profile_interests.js index e69de29bb..a79fb5e1a 100644 --- a/web/app/assets/javascripts/accounts_profile_interests.js +++ b/web/app/assets/javascripts/accounts_profile_interests.js @@ -0,0 +1,116 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.AccountProfileInterests= function(app) { + var $document = $(document); + var logger = context.JK.logger; + var EVENTS = context.JK.EVENTS; + var api = context.JK.Rest(); + var userId; + var user = {}; + var profileUtils = context.JK.ProfileUtils; + + var $screen = $('#account-profile-interests'); + var $scroller = $screen.find('#account-profile-content-scroller'); + var $btnCancel = $scroller.find('#account-edit-profile-cancel'); + var $btnBack = $scroller.find('#account-edit-profile-back'); + var $btnSubmit = $scroller.find('#account-edit-profile-submit'); + + function beforeShow(data) { + userId = data.id; + } + + function afterShow(data) { + renderInterests(); + } + + function resetForm() { + $scroller.find('form .error-text').remove(); + $scroller.find('form .error').removeClass("error"); + } + + function populateAccountProfile(userDetail) { + + + context.JK.dropdown($('select', $scroller)); + } + + function events() { + console.log("HERE2"); + $btnCancel.on('click', function(evt) { evt.stopPropagation(); navigateTo('/client#/account'); return false; } ); + $btnBack.on('click', function(evt) { evt.stopPropagation(); navigateTo('/client#/account/profile/experience'); return false; } ); + $btnSubmit.on('click', function(evt) { evt.stopPropagation(); handleUpdateProfile(); return false; } ); + } + + function renderInterests() { + $.when(api.getUserProfile()) + .done(function(userDetailResponse) { + var userDetail = userDetailResponse[0]; + populateAccountProfile(userDetail); + }); + + context.JK.dropdown($('select')); + } + + function navigateTo(targetLocation) { + console.log("HERE"); + context.location = targetLocation; + } + + function handleUpdateProfile() { + resetForm(); + + api.updateUser({ + instruments: instruments, + genres: genres, + skill_level: $scroller.find('select[name=skill_level]').val(), + concert_count: $scroller.find('select[name=concert_count]').val(), + studio_session_count: $scroller.find('select[name=studio_session_count]').val() + }) + .done(postUpdateProfileSuccess) + .fail(postUpdateProfileFailure); + } + + function postUpdateProfileSuccess(response) { + $document.triggerHandler(EVENTS.USER_UPDATED, response); + context.location = "/client#/account/profile/samples"; + } + + function postUpdateProfileFailure(xhr, textStatus, errorMessage) { + + var errors = JSON.parse(xhr.responseText) + + if(xhr.status == 422) { + + } + else { + app.ajaxError(xhr, textStatus, errorMessage) + } + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + + app.bindScreen('account/profile/interests', screenBindings); + + events(); + + $screen.iCheck({ + checkboxClass: 'icheckbox_minimal', + radioClass: 'iradio_minimal', + inheritClass: true + }); + } + + this.initialize = initialize; + this.beforeShow = beforeShow; + this.afterShow = afterShow; + return this; + }; + +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/accountProfileInterests.css.scss b/web/app/assets/stylesheets/client/accountProfileInterests.css.scss index e69de29bb..0ae312516 100644 --- a/web/app/assets/stylesheets/client/accountProfileInterests.css.scss +++ b/web/app/assets/stylesheets/client/accountProfileInterests.css.scss @@ -0,0 +1,43 @@ +@import "client/common.css.scss"; + + +#account-profile-interests { + .interest { + font-weight: 600; + font-size: 16px; + } + + a.help { + font-weight: normal; + font-size: 14px; + } + + div.genres { + width: 20%; + margin-bottom: 15px; + } + + a.select-genre { + text-decoration: underline; + font-size: 12px; + font-weight: normal !important; + } + + span.genre-list { + font-style: italic; + font-size: 12px; + } + + .interest-options { + width: 30%; + margin-bottom: 15px; + + label { + margin-bottom: 10px; + } + } + + input[type=text].rate { + width: 100px; + } +} \ No newline at end of file diff --git a/web/app/views/clients/_account_profile_experience.html.erb b/web/app/views/clients/_account_profile_experience.html.erb index d4091e602..a371bd594 100644 --- a/web/app/views/clients/_account_profile_experience.html.erb +++ b/web/app/views/clients/_account_profile_experience.html.erb @@ -1,24 +1,17 @@ -
-
-
<%= image_tag "content/icon_account.png", {:width => 27, :height => 20} %>
-

my account

<%= render "screen_navigation" %>
- -
-
diff --git a/web/app/views/layouts/landing.html.erb b/web/app/views/layouts/landing.html.erb index 7ae5a821c..df95fab81 100644 --- a/web/app/views/layouts/landing.html.erb +++ b/web/app/views/layouts/landing.html.erb @@ -75,6 +75,9 @@ <% end %> JK.app = JK.JamKazam(); + JK.getGenreList().done(function(genres) { + JK.genres = genres; + }); JK.app.initialize({inClient: false, layoutOpts: {layoutFooter: false, sizeOverlayToContent: true}}); }) diff --git a/web/app/views/layouts/web.html.erb b/web/app/views/layouts/web.html.erb index e3e828bc4..c816bf518 100644 --- a/web/app/views/layouts/web.html.erb +++ b/web/app/views/layouts/web.html.erb @@ -116,6 +116,10 @@ JK.bindHoverEvents(); + JK.getGenreList().done(function(genres) { + JK.genres = genres; + }); + JK.JamServer.connect() // singleton here defined in JamServer.js .done(function() { console.log("websocket connected") From 9acb235a6dd07c7f70979c31943cf4d15d00aaa5 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 21 Mar 2015 10:04:46 -0400 Subject: [PATCH 071/195] VRFS-2701 VRFS-2699 wip performance samples edit screen --- db/manifest | 1 + .../alter_genre_player_unique_constraint.sql | 2 + .../assets/javascripts/accounts_profile.js | 17 +- .../accounts_profile_experience.js | 23 +- .../javascripts/accounts_profile_interests.js | 26 +- .../javascripts/accounts_profile_samples.js | 70 ++++++ .../javascripts/dialog/genreSelectorDialog.js | 4 +- ...Selector.js => recordingSelectorDialog.js} | 0 web/app/assets/javascripts/ui_helper.js | 7 + ....scss => recordingSelectorDialog.css.scss} | 0 .../clients/_account_profile_samples.html.erb | 223 ++++++++++++++++++ web/app/views/clients/index.html.erb | 4 +- web/app/views/dialogs/_dialogs.html.haml | 3 +- ....erb => _recordingSelectorDialog.html.erb} | 0 14 files changed, 366 insertions(+), 14 deletions(-) create mode 100644 db/up/alter_genre_player_unique_constraint.sql rename web/app/assets/javascripts/dialog/{recordingSelector.js => recordingSelectorDialog.js} (100%) rename web/app/assets/stylesheets/dialogs/{recordingSelector.css.scss => recordingSelectorDialog.css.scss} (100%) rename web/app/views/dialogs/{_recordingSelector.html.erb => _recordingSelectorDialog.html.erb} (100%) diff --git a/db/manifest b/db/manifest index 761758fe2..4e3709a27 100755 --- a/db/manifest +++ b/db/manifest @@ -268,3 +268,4 @@ jam_track_redeemed.sql connection_metronome.sql preview_jam_track_tracks.sql cohorts.sql +alter_genre_player_unique_constraint.sql \ No newline at end of file diff --git a/db/up/alter_genre_player_unique_constraint.sql b/db/up/alter_genre_player_unique_constraint.sql new file mode 100644 index 000000000..c0159b59b --- /dev/null +++ b/db/up/alter_genre_player_unique_constraint.sql @@ -0,0 +1,2 @@ +ALTER TABLE genre_players DROP CONSTRAINT genre_player_uniqkey; +ALTER TABLE genre_players ADD CONSTRAINT genre_player_uniqkey UNIQUE (player_id, player_type, genre_id, genre_type); \ No newline at end of file diff --git a/web/app/assets/javascripts/accounts_profile.js b/web/app/assets/javascripts/accounts_profile.js index c418c213f..b7189774a 100644 --- a/web/app/assets/javascripts/accounts_profile.js +++ b/web/app/assets/javascripts/accounts_profile.js @@ -18,6 +18,9 @@ var nilOptionStr = ''; var nilOptionText = 'n/a'; var $screen = $('#account-profile-basics'); + var $btnCancel = $screen.find('#account-edit-profile-cancel'); + var $btnSubmit = $screen.find('#account-edit-profile-submit'); + var $biography = null; function beforeShow(data) { @@ -213,8 +216,18 @@ /****************** MAIN PORTION OF SCREEN *****************/ // events for main screen function events() { - $('#account-profile-content-scroller').on('click', '#account-edit-profile-cancel', function(evt) { evt.stopPropagation(); navToAccount(); return false; } ); - $('#account-profile-content-scroller').on('click', '#account-edit-profile-submit', function(evt) { evt.stopPropagation(); handleUpdateProfile(); return false; } ); + $btnCancel.click(function(evt) { + evt.stopPropagation(); + navToAccount(); + return false; + }); + + $btnSubmit.click(function(evt) { + evt.stopPropagation(); + handleUpdateProfile(); + return false; + }); + $('#account-profile-content-scroller').on('submit', '#account-edit-email-form', function(evt) { evt.stopPropagation(); handleUpdateProfile(); return false; } ); $('#account-profile-content-scroller').on('click', '#account-change-avatar', function(evt) { evt.stopPropagation(); navToAvatar(); return false; } ); } diff --git a/web/app/assets/javascripts/accounts_profile_experience.js b/web/app/assets/javascripts/accounts_profile_experience.js index 2a212acd5..3e0b1efb6 100644 --- a/web/app/assets/javascripts/accounts_profile_experience.js +++ b/web/app/assets/javascripts/accounts_profile_experience.js @@ -13,6 +13,9 @@ var $instrumentSelector = null; var $userGenres = null; var profileUtils = context.JK.ProfileUtils; + var $btnCancel = $screen.find('#account-edit-profile-cancel'); + var $btnBack = $screen.find('#account-edit-profile-back'); + var $btnSubmit = $screen.find('#account-edit-profile-submit'); function beforeShow(data) { } @@ -118,9 +121,23 @@ } function events() { - $screen.find('#account-edit-profile-cancel').on('click', function(evt) { evt.stopPropagation(); navigateTo('/client#/profile/' + context.JK.currentUserId); return false; } ); - $screen.find('#account-edit-profile-back').on('click', function(evt) { evt.stopPropagation(); navigateTo('/client#/account/profile/'); return false; } ); - $screen.find('#account-edit-profile-submit').on('click', function(evt) { evt.stopPropagation(); handleUpdateProfile(); return false; } ); + $btnCancel.click(function(evt) { + evt.stopPropagation(); + navigateTo('/client#/profile/' + context.JK.currentUserId); + return false; + }); + + $btnBack.click(function(evt) { + evt.stopPropagation(); + navigateTo('/client#/account/profile/'); + return false; + }); + + $btnSubmit.click(function(evt) { + evt.stopPropagation(); + handleUpdateProfile(); + return false; + }); } function renderExperience() { diff --git a/web/app/assets/javascripts/accounts_profile_interests.js b/web/app/assets/javascripts/accounts_profile_interests.js index 2edbf5f59..6d8ccd40b 100644 --- a/web/app/assets/javascripts/accounts_profile_interests.js +++ b/web/app/assets/javascripts/accounts_profile_interests.js @@ -3,7 +3,7 @@ "use strict"; context.JK = context.JK || {}; - context.JK.AccountProfileInterests= function(app) { + context.JK.AccountProfileInterests = function(app) { var $document = $(document); var logger = context.JK.logger; var EVENTS = context.JK.EVENTS; @@ -154,7 +154,7 @@ function bindGenreSelector(type, $btnSelect, $genreList) { $btnSelect.unbind('click').click(function(evt) { - evt.stopPropagation(); + evt.preventDefault(); var genreText = $genreList.html(); var genres = []; if (genres !== NONE_SPECIFIED) { @@ -164,6 +164,8 @@ ui.launchGenreSelectorDialog(type, genres, function(selectedGenres) { $genreList.html(selectedGenres && selectedGenres.length > 0 ? selectedGenres.join(GENRE_LIST_DELIMITER) : NONE_SPECIFIED); }); + + return false; }); } @@ -175,9 +177,23 @@ bindGenreSelector('free sessions', $btnFreeSessionsGenreSelect, $freeSessionsGenreList); bindGenreSelector('co-writing', $btnCowritingGenreSelect, $cowritingGenreList); - $btnCancel.on('click', function(evt) { evt.stopPropagation(); navigateTo('/client#/profile/' + context.JK.currentUserId); return false; } ); - $btnBack.on('click', function(evt) { evt.stopPropagation(); navigateTo('/client#/account/profile/experience'); return false; } ); - $btnSubmit.on('click', function(evt) { evt.stopPropagation(); handleUpdateProfile(); return false; } ); + $btnCancel.click(function(evt) { + evt.stopPropagation(); + navigateTo('/client#/profile/' + context.JK.currentUserId); + return false; + }); + + $btnBack.click(function(evt) { + evt.stopPropagation(); + navigateTo('/client#/account/profile/experience'); + return false; + }); + + $btnSubmit.click(function(evt) { + evt.stopPropagation(); + handleUpdateProfile(); + return false; + }); context.JK.dropdown($virtualBandCommitment); context.JK.dropdown($traditionalBandCommitment); diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index e69de29bb..0131f49e4 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -0,0 +1,70 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.AccountProfileSamples = function(app) { + var $document = $(document); + var logger = context.JK.logger; + var EVENTS = context.JK.EVENTS; + var api = context.JK.Rest(); + var ui = new context.JK.UIHelper(JK.app); + var user = {}; + var profileUtils = context.JK.ProfileUtils; + + var $screen = $('#account-profile-samples'); + var $btnAddJkRecording = $screen.find('') + + function beforeShow(data) { + } + + function afterShow(data) { + renderSamples(); + } + + function renderSamples() { + + } + + function events() { + $btnAddJkRecording.click(function(evt) { + evt.preventDefault(); + ui.launchRecordingSelectorDialog('', function(selectedRecordings) { + + }); + + return false; + }); + + $btnAddSoundCloudRecording.click(function(evt) { + + }); + + $btnAddYouTubeVideo.click(function(evt) { + + }); + + $btnSaveAndFinish.click(function(evt) { + $document.triggerHandler(EVENTS.USER_UPDATED, response); + context.location = "/client#/profile/" + context.JK.currentUserId; + }); + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + + app.bindScreen('account/profile/samples', screenBindings); + + events(); + } + + this.initialize = initialize; + this.beforeShow = beforeShow; + this.afterShow = afterShow; + return this; + }; + +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/dialog/genreSelectorDialog.js b/web/app/assets/javascripts/dialog/genreSelectorDialog.js index 78f8c08dc..f72634d0c 100644 --- a/web/app/assets/javascripts/dialog/genreSelectorDialog.js +++ b/web/app/assets/javascripts/dialog/genreSelectorDialog.js @@ -57,7 +57,7 @@ function events() { $btnSelect.click(function(evt) { - + evt.preventDefault(); var selectedGenres = []; $genres.find('input[type=checkbox]:checked').each(function(index) { selectedGenres.push($(this).val()); @@ -69,6 +69,8 @@ app.layout.closeDialog(dialogId); + return false; + }); } diff --git a/web/app/assets/javascripts/dialog/recordingSelector.js b/web/app/assets/javascripts/dialog/recordingSelectorDialog.js similarity index 100% rename from web/app/assets/javascripts/dialog/recordingSelector.js rename to web/app/assets/javascripts/dialog/recordingSelectorDialog.js diff --git a/web/app/assets/javascripts/ui_helper.js b/web/app/assets/javascripts/ui_helper.js index ed0621e8e..bdebf103d 100644 --- a/web/app/assets/javascripts/ui_helper.js +++ b/web/app/assets/javascripts/ui_helper.js @@ -68,6 +68,12 @@ return genreSelectorDialog.showDialog(); } + function launchRecordingSelectorDialog(recordings, callback) { + var recordingSelectorDialog = new JK.RecordingSelectorDialog(JK.app, recordings, callback); + recordingSelectorDialog.initialize(); + return recordingSelectorDialog.showDialog(); + } + this.addSessionLike = addSessionLike; this.addRecordingLike = addRecordingLike; this.launchCommentDialog = launchCommentDialog; @@ -77,6 +83,7 @@ this.launchRsvpCreateSlotDialog = launchRsvpCreateSlotDialog; this.launchSessionStartDialog = launchSessionStartDialog; this.launchGenreSelectorDialog = launchGenreSelectorDialog; + this.launchRecordingSelectorDialog = launchRecordingSelectorDialog; return this; }; diff --git a/web/app/assets/stylesheets/dialogs/recordingSelector.css.scss b/web/app/assets/stylesheets/dialogs/recordingSelectorDialog.css.scss similarity index 100% rename from web/app/assets/stylesheets/dialogs/recordingSelector.css.scss rename to web/app/assets/stylesheets/dialogs/recordingSelectorDialog.css.scss diff --git a/web/app/views/clients/_account_profile_samples.html.erb b/web/app/views/clients/_account_profile_samples.html.erb index e69de29bb..bb2d80636 100644 --- a/web/app/views/clients/_account_profile_samples.html.erb +++ b/web/app/views/clients/_account_profile_samples.html.erb @@ -0,0 +1,223 @@ +
+
+
+ <%= image_tag "content/icon_account.png", {:width => 27, :height => 20} %> +
+

my account

+ <%= render "screen_navigation" %> +
+ +
+ +
+
+ diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index c6e4bbea8..1757ddcf0 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -220,8 +220,8 @@ var accountProfileInterests = new JK.AccountProfileInterests(JK.app); accountProfileInterests.initialize(); - // var accountProfileSamples = new JK.AccountProfileSamples(JK.app); - // accountProfileSamples.initialize(); + var accountProfileSamples = new JK.AccountProfileSamples(JK.app); + accountProfileSamples.initialize(); var accountAudioProfile = new JK.AccountAudioProfile(JK.app); accountAudioProfile.initialize(); diff --git a/web/app/views/dialogs/_dialogs.html.haml b/web/app/views/dialogs/_dialogs.html.haml index 809181660..7383a4400 100644 --- a/web/app/views/dialogs/_dialogs.html.haml +++ b/web/app/views/dialogs/_dialogs.html.haml @@ -34,4 +34,5 @@ = render 'dialogs/adjustGearSpeedDialog' = render 'dialogs/openJamTrackDialog' = render 'dialogs/openBackingTrackDialog' -= render 'dialogs/genreSelectorDialog' \ No newline at end of file += render 'dialogs/genreSelectorDialog' += render 'dialogs/recordingSelectorDialog' \ No newline at end of file diff --git a/web/app/views/dialogs/_recordingSelector.html.erb b/web/app/views/dialogs/_recordingSelectorDialog.html.erb similarity index 100% rename from web/app/views/dialogs/_recordingSelector.html.erb rename to web/app/views/dialogs/_recordingSelectorDialog.html.erb From ae71c1b9e90c83c1fca24f8566b1962312f74129 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 21 Mar 2015 10:16:46 -0400 Subject: [PATCH 072/195] VRFS-2701 VRFS-2699 wip performance samples edit screen --- .../javascripts/accounts_profile_samples.js | 6 +- .../dialog/recordingSelectorDialog.js | 68 +++++++++++++++++++ web/app/assets/javascripts/ui_helper.js | 4 +- .../dialogs/recordingSelectorDialog.css.scss | 14 ++++ .../dialogs/_recordingSelectorDialog.html.erb | 14 ++++ .../_recordingSelectorDialog.html.haml | 0 6 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 web/app/views/dialogs/_recordingSelectorDialog.html.haml diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 0131f49e4..6938b57fe 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -13,7 +13,11 @@ var profileUtils = context.JK.ProfileUtils; var $screen = $('#account-profile-samples'); - var $btnAddJkRecording = $screen.find('') + //var $btnAddJkRecording = $screen.find(''); + + var $btnCancel = $screen.find('#account-edit-profile-cancel'); + var $btnBack = $screen.find('#account-edit-profile-back'); + var $btnSubmit = $screen.find('#account-edit-profile-submit'); function beforeShow(data) { } diff --git a/web/app/assets/javascripts/dialog/recordingSelectorDialog.js b/web/app/assets/javascripts/dialog/recordingSelectorDialog.js index e69de29bb..cc485aa64 100644 --- a/web/app/assets/javascripts/dialog/recordingSelectorDialog.js +++ b/web/app/assets/javascripts/dialog/recordingSelectorDialog.js @@ -0,0 +1,68 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.RecordingSelectorDialog = function(app, recordings, selectedRecordings, callback) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var $dialog = null; + var dialogId = 'recording-selector-dialog'; + var $screen = $('#' + dialogId); + var $btnSelect = $screen.find(".btn-select-recordings"); + var $instructions = $screen.find('#instructions'); + var $recordings = $screen.find('.recordings'); + + function beforeShow(data) { + } + + function afterShow(data) { + + $recordings.empty(); + } + + function afterHide() { + } + + function showDialog() { + return app.layout.showDialog(dialogId); + } + + function events() { + $btnSelect.click(function(evt) { + evt.preventDefault(); + var selectedRecordings = []; + $recordings.find('input[type=checkbox]:checked').each(function(index) { + selectedRecordings.push($(this).val()); + }); + + if (callback) { + callback(selectedRecordings); + } + + app.layout.closeDialog(dialogId); + + return false; + + }); + } + + function initialize() { + var dialogBindings = { + 'beforeShow' : beforeShow, + 'afterShow' : afterShow, + 'afterHide': afterHide + }; + + app.bindDialog(dialogId, dialogBindings); + + $instructions.html('Select one or more recordings.'); + + events(); + } + + this.initialize = initialize; + this.showDialog = showDialog; + } + + return this; +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/ui_helper.js b/web/app/assets/javascripts/ui_helper.js index bdebf103d..01d2aa403 100644 --- a/web/app/assets/javascripts/ui_helper.js +++ b/web/app/assets/javascripts/ui_helper.js @@ -68,8 +68,8 @@ return genreSelectorDialog.showDialog(); } - function launchRecordingSelectorDialog(recordings, callback) { - var recordingSelectorDialog = new JK.RecordingSelectorDialog(JK.app, recordings, callback); + function launchRecordingSelectorDialog(recordings, selectedRecordings, callback) { + var recordingSelectorDialog = new JK.RecordingSelectorDialog(JK.app, recordings, selectedRecordings, callback); recordingSelectorDialog.initialize(); return recordingSelectorDialog.showDialog(); } diff --git a/web/app/assets/stylesheets/dialogs/recordingSelectorDialog.css.scss b/web/app/assets/stylesheets/dialogs/recordingSelectorDialog.css.scss index e69de29bb..c23ea586f 100644 --- a/web/app/assets/stylesheets/dialogs/recordingSelectorDialog.css.scss +++ b/web/app/assets/stylesheets/dialogs/recordingSelectorDialog.css.scss @@ -0,0 +1,14 @@ +@import "client/common"; + +#genre-selector-dialog { + + min-height:initial; + + .dialog-inner { + color:white; + } + + .action-buttons { + margin-bottom:10px; + } +} \ No newline at end of file diff --git a/web/app/views/dialogs/_recordingSelectorDialog.html.erb b/web/app/views/dialogs/_recordingSelectorDialog.html.erb index e69de29bb..a3e1cc93b 100644 --- a/web/app/views/dialogs/_recordingSelectorDialog.html.erb +++ b/web/app/views/dialogs/_recordingSelectorDialog.html.erb @@ -0,0 +1,14 @@ +.dialog.dialog-overlay-sm{layout: 'dialog', 'layout-id' => 'recording-selector-dialog', id: 'recording-selector-dialog'} + .content-head + = image_tag "content/icon_checkmark_circle.png", {:width => 20, :height => 20, :class => 'content-icon' } + %h1 + = 'select recordings' + .dialog-inner + %span{id: 'instructions'} + %br{:clear => "all"}/ + %br{:clear => "all"}/ + .recordings + + .right.action-buttons + %a.button-grey.btn-cancel-dialog{'layout-action' => 'cancel'} CANCEL + %a.button-orange.btn-select-recordings SELECT \ No newline at end of file diff --git a/web/app/views/dialogs/_recordingSelectorDialog.html.haml b/web/app/views/dialogs/_recordingSelectorDialog.html.haml new file mode 100644 index 000000000..e69de29bb From 370ca1086ed5dad803d385a85d72273673a7f098 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 21 Mar 2015 20:53:38 -0400 Subject: [PATCH 073/195] VRFS-2701 VRFS-2699 wip performance samples edit screen --- .../javascripts/accounts_profile_samples.js | 28 +++++++++---------- .../dialogs/_recordingSelectorDialog.html.erb | 14 ---------- .../_recordingSelectorDialog.html.haml | 14 ++++++++++ 3 files changed, 28 insertions(+), 28 deletions(-) delete mode 100644 web/app/views/dialogs/_recordingSelectorDialog.html.erb diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 6938b57fe..5ef8e299f 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -31,27 +31,27 @@ } function events() { - $btnAddJkRecording.click(function(evt) { - evt.preventDefault(); - ui.launchRecordingSelectorDialog('', function(selectedRecordings) { + // $btnAddJkRecording.click(function(evt) { + // evt.preventDefault(); + // ui.launchRecordingSelectorDialog('', function(selectedRecordings) { - }); + // }); - return false; - }); + // return false; + // }); - $btnAddSoundCloudRecording.click(function(evt) { + // $btnAddSoundCloudRecording.click(function(evt) { - }); + // }); - $btnAddYouTubeVideo.click(function(evt) { + // $btnAddYouTubeVideo.click(function(evt) { - }); + // }); - $btnSaveAndFinish.click(function(evt) { - $document.triggerHandler(EVENTS.USER_UPDATED, response); - context.location = "/client#/profile/" + context.JK.currentUserId; - }); + // $btnSaveAndFinish.click(function(evt) { + // $document.triggerHandler(EVENTS.USER_UPDATED, response); + // context.location = "/client#/profile/" + context.JK.currentUserId; + // }); } function initialize() { diff --git a/web/app/views/dialogs/_recordingSelectorDialog.html.erb b/web/app/views/dialogs/_recordingSelectorDialog.html.erb deleted file mode 100644 index a3e1cc93b..000000000 --- a/web/app/views/dialogs/_recordingSelectorDialog.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -.dialog.dialog-overlay-sm{layout: 'dialog', 'layout-id' => 'recording-selector-dialog', id: 'recording-selector-dialog'} - .content-head - = image_tag "content/icon_checkmark_circle.png", {:width => 20, :height => 20, :class => 'content-icon' } - %h1 - = 'select recordings' - .dialog-inner - %span{id: 'instructions'} - %br{:clear => "all"}/ - %br{:clear => "all"}/ - .recordings - - .right.action-buttons - %a.button-grey.btn-cancel-dialog{'layout-action' => 'cancel'} CANCEL - %a.button-orange.btn-select-recordings SELECT \ No newline at end of file diff --git a/web/app/views/dialogs/_recordingSelectorDialog.html.haml b/web/app/views/dialogs/_recordingSelectorDialog.html.haml index e69de29bb..a3e1cc93b 100644 --- a/web/app/views/dialogs/_recordingSelectorDialog.html.haml +++ b/web/app/views/dialogs/_recordingSelectorDialog.html.haml @@ -0,0 +1,14 @@ +.dialog.dialog-overlay-sm{layout: 'dialog', 'layout-id' => 'recording-selector-dialog', id: 'recording-selector-dialog'} + .content-head + = image_tag "content/icon_checkmark_circle.png", {:width => 20, :height => 20, :class => 'content-icon' } + %h1 + = 'select recordings' + .dialog-inner + %span{id: 'instructions'} + %br{:clear => "all"}/ + %br{:clear => "all"}/ + .recordings + + .right.action-buttons + %a.button-grey.btn-cancel-dialog{'layout-action' => 'cancel'} CANCEL + %a.button-orange.btn-select-recordings SELECT \ No newline at end of file From e4aa6609ec8ace1fbae3ae8f45c1cf6cfe273a8f Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sun, 22 Mar 2015 21:13:53 -0400 Subject: [PATCH 074/195] VRFS-2974 fixed broadcast widget bug when session is private --- .../javascripts/jquery.listenbroadcast.js | 54 +++++++++++-------- .../api_music_sessions_controller.rb | 3 +- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/web/app/assets/javascripts/jquery.listenbroadcast.js b/web/app/assets/javascripts/jquery.listenbroadcast.js index 059b7c18b..f6964cd60 100644 --- a/web/app/assets/javascripts/jquery.listenbroadcast.js +++ b/web/app/assets/javascripts/jquery.listenbroadcast.js @@ -74,6 +74,9 @@ var playState = PlayStateNone; // tracks if the stream is actually playing + var CANNOT_BROADCAST_TITLE = 'Unable to Broadcast Session'; + var CANNOT_BROADCAST_MSG = 'This session cannot be broadcasted. The session organizer may have configured it to be private.'; + function play(e) { if(e) { e.preventDefault(); @@ -407,17 +410,21 @@ sessionInfo = response; }) .fail(function(jqXHR) { - if(jqXHR.status == 404 || jqXHR.status == 403) { + if(jqXHR.status == 404) { transition(PlayStateSessionOver); destroy(); } + else if (jqXHR.status == 403) { + logger.debug("session is private"); + context.JK.app.notify({"title": CANNOT_BROADCAST_TITLE, "text": CANNOT_BROADCAST_MSG}); + } else if(jqXHR.status >= 500 && jqXHR.status <= 599){ transition(PlayStateServerError); } else { transition(PlayStateNetworkError); } - }) + }); } function triggerStateChange() { @@ -702,28 +709,31 @@ } function openBubble() { - checkServer().done(function(response) { + checkServer() + .done(function(response) { - var mountId = sessionInfo.mount ? sessionInfo.mount.id : null; + var mountId = sessionInfo.mount ? sessionInfo.mount.id : null; - if(mountId) { - rest.getMount({id: mountId}) - .done(function (mount) { - mountInfo = mount; - $parent.data('mount-id', mountId); - context.JK.SubscriptionUtils.subscribe('mount', mountId).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, onDetailEvent); - $parent.btOn(); - }) - .fail(context.JK.app.ajaxError) - } - else { - mountInfo = null; - destroy(); - context.JK.app.notify({"title": "Unable to Broadcast Session", "text": "This session cannot be broadcasted. The session organizer may have configured it to be private."}); - } - }) - .fail(function() { - logger.debug("session is over") + if(mountId) { + rest.getMount({id: mountId}) + .done(function (mount) { + mountInfo = mount; + $parent.data('mount-id', mountId); + context.JK.SubscriptionUtils.subscribe('mount', mountId).on(context.JK.EVENTS.SUBSCRIBE_NOTIFICATION, onDetailEvent); + $parent.btOn(); + }) + .fail(context.JK.app.ajaxError) + } + else { + mountInfo = null; + destroy(); + context.JK.app.notify({"title": CANNOT_BROADCAST_TITLE, "text": CANNOT_BROADCAST_MSG}); + } + }) + .fail(function(response) { + if (response.status == 404) { + logger.debug("session is over"); + } }) } diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index bb8760dc2..6fb5df5cf 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -165,7 +165,8 @@ class ApiMusicSessionsController < ApiController def show unless @music_session.can_see? current_user - raise ActiveRecord::RecordNotFound + # render :json => { :message => ValidationMessages::PERMISSION_VALIDATION_ERROR }, :status => 403 + raise JamRuby::PermissionError end end From 231f7c69b51a3734ff5ed807284fb2115b9eabbb Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 23 Mar 2015 13:11:28 -0400 Subject: [PATCH 075/195] VRFS-2701 wip presence/sample edit --- .../javascripts/accounts_profile_samples.js | 120 +++++++-- .../client/accountProfileInterests.css.scss | 1 - .../client/accountProfileSamples.css.scss | 33 +++ .../dialogs/recordingSelectorDialog.css.scss | 2 +- .../api_music_sessions_controller.rb | 1 - .../clients/_account_profile_samples.html.erb | 245 ++++++------------ 6 files changed, 210 insertions(+), 192 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 5ef8e299f..3b4c6568e 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -13,7 +13,18 @@ var profileUtils = context.JK.ProfileUtils; var $screen = $('#account-profile-samples'); - //var $btnAddJkRecording = $screen.find(''); + var $url = $screen.find('#url'); + var $soundCloudUsername = $screen.find('#soundcloud-username'); + var $reverbNationUsername = $screen.find('#reverbnation-username'); + var $bandCampUsername = $screen.find('#bandcamp-username'); + var $fandalismUsername = $screen.find('#fandalism-username'); + var $youTubeUsername = $screen.find('#youtube-username'); + var $facebookUsername = $screen.find('#facebook-username'); + var $twitterUsername = $screen.find('#twitter-username'); + + var $btnAddJkRecording = $screen.find('#btn-add-jk-recording'); + var $btnAddSoundCloudRecording = $screen.find('#btn-add-soundcloud-recording'); + var $btnAddYouTubeVideo = $screen.find('#btn-add-youtube-video'); var $btnCancel = $screen.find('#account-edit-profile-cancel'); var $btnBack = $screen.find('#account-edit-profile-back'); @@ -23,35 +34,108 @@ } function afterShow(data) { - renderSamples(); + $.when(api.getUserProfile()) + .done(function(userDetail) { + renderPresence(userDetail); + renderSamples(userDetail); + }); } - function renderSamples() { + function renderPresence(user) { + $url.val(user.website); + // SoundCloud + var presences = profileUtils.soundCloudPresences(user.online_presences); + if (presences && presences.length > 0) { + $soundCloudUsername.val(presences[0].username); + } + + // ReverbNation + presences = profileUtils.reverbNationPresences(user.online_presences); + if (presences && presences.length > 0) { + $reverbNationUsername.val(presences[0].username); + } + + // Bandcamp + presences = profileUtils.bandCampPresences(user.online_presences); + if (presences && presences.length > 0) { + $bandCampUsername.val(presences[0].username); + } + + // Fandalism + presences = profileUtils.fandalismPresences(user.online_presences); + if (presences && presences.length > 0) { + $fandalismUsername.val(presences[0].username); + } + + // YouTube + presences = profileUtils.youTubePresences(user.online_presences); + if (presences && presences.length > 0) { + $youTubeUsername.val(presences[0].username); + } + + // Facebook + presences = profileUtils.facebookPresences(user.online_presences); + if (presences && presences.length > 0) { + $facebookUsername.val(presences[0].username); + } + + // Twitter + presences = profileUtils.twitterPresences(user.online_presences); + if (presences && presences.length > 0) { + $twitterUsername.val(presences[0].username); + } + } + + function renderSamples(user) { + + // JamKazam recordings + var samples = profileUtils.jamkazamSamples(user.performance_samples); + if (samples) { + $.each(samples, function(index, val) { + + }); + } + + // SoundCloud recordings + samples = profileUtils.soundCloudSamples(user.performance_samples); + if (samples) { + $.each(samples, function(index, val) { + + }); + } + + // YouTube videos + samples = profileUtils.youTubeSamples(user.performance_samples); + if (samples) { + $.each(samples, function(index, val) { + + }); + } } function events() { - // $btnAddJkRecording.click(function(evt) { - // evt.preventDefault(); - // ui.launchRecordingSelectorDialog('', function(selectedRecordings) { + $btnAddJkRecording.click(function(evt) { + evt.preventDefault(); + ui.launchRecordingSelectorDialog('', function(selectedRecordings) { - // }); + }); - // return false; - // }); + return false; + }); - // $btnAddSoundCloudRecording.click(function(evt) { + $btnAddSoundCloudRecording.click(function(evt) { + // add to list + }); - // }); + $btnAddYouTubeVideo.click(function(evt) { - // $btnAddYouTubeVideo.click(function(evt) { + }); - // }); - - // $btnSaveAndFinish.click(function(evt) { - // $document.triggerHandler(EVENTS.USER_UPDATED, response); - // context.location = "/client#/profile/" + context.JK.currentUserId; - // }); + $btnSubmit.click(function(evt) { + $document.triggerHandler(EVENTS.USER_UPDATED, response); + context.location = "/client#/profile/" + context.JK.currentUserId; + }); } function initialize() { diff --git a/web/app/assets/stylesheets/client/accountProfileInterests.css.scss b/web/app/assets/stylesheets/client/accountProfileInterests.css.scss index 230a1f4b0..7e11df987 100644 --- a/web/app/assets/stylesheets/client/accountProfileInterests.css.scss +++ b/web/app/assets/stylesheets/client/accountProfileInterests.css.scss @@ -1,6 +1,5 @@ @import "common.css.scss"; - #account-profile-interests { .interest { font-weight: 600; diff --git a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss index e69de29bb..134b1cc3c 100644 --- a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss +++ b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss @@ -0,0 +1,33 @@ +@import "common.css.scss"; + +#account-profile-samples { + + .sample { + font-weight: 600; + font-size: 16px; + } + + .presence { + margin: 3px 30px 15px 0px; + + input { + width:200px; + } + } + + .samples { + width: 30%; + + input { + width: 200px; + } + + .sample { + margin: 3px 5px 10px 0px; + } + + .sample-list { + + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/dialogs/recordingSelectorDialog.css.scss b/web/app/assets/stylesheets/dialogs/recordingSelectorDialog.css.scss index c23ea586f..c9994cff9 100644 --- a/web/app/assets/stylesheets/dialogs/recordingSelectorDialog.css.scss +++ b/web/app/assets/stylesheets/dialogs/recordingSelectorDialog.css.scss @@ -1,6 +1,6 @@ @import "client/common"; -#genre-selector-dialog { +#recording-selector-dialog { min-height:initial; diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index 6fb5df5cf..50100a0a2 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -165,7 +165,6 @@ class ApiMusicSessionsController < ApiController def show unless @music_session.can_see? current_user - # render :json => { :message => ValidationMessages::PERMISSION_VALIDATION_ERROR }, :status => 403 raise JamRuby::PermissionError end end diff --git a/web/app/views/clients/_account_profile_samples.html.erb b/web/app/views/clients/_account_profile_samples.html.erb index bb2d80636..708e41557 100644 --- a/web/app/views/clients/_account_profile_samples.html.erb +++ b/web/app/views/clients/_account_profile_samples.html.erb @@ -16,194 +16,97 @@

edit profile: online presence & performance samples

-
- -
-
- -
-
- -
-
-
-
- -
-
- -
+
+ +
+
- -
- - +
+ +
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
-
- - +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
-
- -
-
- -
-
- -
+
+ +
+ BROWSE
-
-
- -
-
- -
+
+
scrollable
+
+
+ +
+
+
+ ADD +
+
+
scrollable
- -
- - -
- -
- - -
- -
- - +
+ +
+ +
+
+ ADD +
+
+
scrollable
- -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
-
- - - -
- - -
- -
- - -
-
- -
- -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
-
- -
- - -
-
- -
- -
-
- -
-
- -
-
- -
-
-
-
- -
-
- -
-
-
- -
- - -
- -
- - -
-
-

From 9face3b3f9dd5d1f639249801637de0cefada6dc Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 23 Mar 2015 16:27:36 -0400 Subject: [PATCH 076/195] VRFS-2701 wip presence/sample edit --- ruby/lib/jam_ruby/models/user.rb | 4 +- .../javascripts/accounts_profile_samples.js | 136 +++++++++++++++++- web/app/assets/javascripts/profile_utils.js | 3 +- web/app/views/api_users/profile_show.rabl | 2 +- .../clients/_account_profile_samples.html.erb | 29 +++- 5 files changed, 161 insertions(+), 13 deletions(-) diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 0d6661b75..f2810d985 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -658,7 +658,7 @@ module JamRuby end online_presences.each do |op| - op = OnlinePresence.create(self.id, online_presences, false) + op = OnlinePresence.create(self, online_presences, false) self.online_presences << op end end @@ -669,7 +669,7 @@ module JamRuby end performance_samples.each do |ps| - ps = PerformanceSample.create(self.id, performance_samples, false) + ps = PerformanceSample.create(self, performance_samples, false) self.performance_samples << ps end end diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 3b4c6568e..694039d5d 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -13,7 +13,9 @@ var profileUtils = context.JK.ProfileUtils; var $screen = $('#account-profile-samples'); - var $url = $screen.find('#url'); + + // online presences + var $website = $screen.find('#website'); var $soundCloudUsername = $screen.find('#soundcloud-username'); var $reverbNationUsername = $screen.find('#reverbnation-username'); var $bandCampUsername = $screen.find('#bandcamp-username'); @@ -22,6 +24,15 @@ var $facebookUsername = $screen.find('#facebook-username'); var $twitterUsername = $screen.find('#twitter-username'); + // performance samples + var $soundCloudRecordingUrl = $screen.find('#soundcloud-recording'); + var $youTubeVideoUrl = $screen.find('#youtube-video'); + + var $jamkazamSampleList = $screen.find('.samples.jamkazam'); + var $soundCloudSampleList = $screen.find('.samples.soundcloud'); + var $youTubeSampleList = $screen.find('.samples.youtube'); + + // buttons var $btnAddJkRecording = $screen.find('#btn-add-jk-recording'); var $btnAddSoundCloudRecording = $screen.find('#btn-add-soundcloud-recording'); var $btnAddYouTubeVideo = $screen.find('#btn-add-youtube-video'); @@ -42,7 +53,7 @@ } function renderPresence(user) { - $url.val(user.website); + $website.val(user.website); // SoundCloud var presences = profileUtils.soundCloudPresences(user.online_presences); @@ -115,6 +126,49 @@ } function events() { + + $website.blur(function(evt) { + evt.preventDefault(); + // TODO: validate website + }); + + $soundCloudUsername.blur(function(evt) { + evt.preventDefault(); + // TODO: validate SoundCloud username + }); + + $reverbNationUsername.blur(function(evt) { + evt.preventDefault(); + // TODO: validate ReverbNation username + }); + + $bandCampUsername.blur(function(evt) { + evt.preventDefault(); + // TODO: validate Bandcamp username + }); + + $fandalismUsername.blur(function(evt) { + evt.preventDefault(); + // TODO: validate Fandalism username + }); + + $youTubeUsername.blur(function(evt) { + evt.preventDefault(); + // TODO: validate YouTube username + }); + + $facebookUsername.blur(function(evt) { + evt.preventDefault(); + // TODO: validate Facebook username + }); + + $twitterUsername.blur(function(evt) { + evt.preventDefault(); + // TODO: validate Twitter username + }); + + + // buttons $btnAddJkRecording.click(function(evt) { evt.preventDefault(); ui.launchRecordingSelectorDialog('', function(selectedRecordings) { @@ -125,19 +179,91 @@ }); $btnAddSoundCloudRecording.click(function(evt) { - // add to list + var url = $soundCloudRecordingUrl.val(); + if (url.length > 0) { + if (extractSoundCloudUrlParts(url)) { + // add to list + } + } }); $btnAddYouTubeVideo.click(function(evt) { - + var url = $youTubeVideoUrl.val(); + if (url.length) { + if (extractYouTubeUrlParts(url)) { + // add to list + } + } }); $btnSubmit.click(function(evt) { $document.triggerHandler(EVENTS.USER_UPDATED, response); - context.location = "/client#/profile/" + context.JK.currentUserId; + + handleUpdateProfile(); + return false; }); } + function addOnlinePresence(presenceArray, username, type) { + if ($.trim($soundCloudUsername.val()).length > 0) { + presenceArray.push({ + service_type: type, + username: username + }); + } + } + + function addPerformanceSamples(sampleArray, $samplesSelector, type) { + var sampleTable = $samplesSelector.find('table'); + // loop over rows, extracting service id, description, and url + } + + function handleUpdateProfile() { + + // extract online presences + var op = []; + var presenceTypes = profileUtils.ONLINE_PRESENCE_TYPES; + addOnlinePresence(op, $soundCloudUsername.val(), presenceTypes.SOUNDCLOUD.description); + addOnlinePresence(op, $reverbNationUsername.val(), presenceTypes.REVERBNATION.description); + addOnlinePresence(op, $bandCampUsername.val(), presenceTypes.BANDCAMP.description); + addOnlinePresence(op, $fandalismUsername.val(), presenceTypes.FANDALISM.description); + addOnlinePresence(op, $youTubeUsername.val(), presenceTypes.YOUTUBE.description); + addOnlinePresence(op, $facebookUsername.val(), presenceTypes.FACEBOOK.description); + addOnlinePresence(op, $twitterUsername.val(), presenceTypes.TWITTER.description); + + // extract performance samples + var ps = []; + var performanceSampleTypes = profileUtils.SAMPLE_TYPES; + addPerformanceSamples(ps, $jamkazamSampleList, performanceSampleTypes.JAMKAZAM); + addPerformanceSamples(ps, $soundCloudSampleList, performanceSampleTypes.SOUNDCLOUD); + addPerformanceSamples(ps, $youTubeSampleList, performanceSampleTypes.YOUTUBE); + + // api.updateUser({ + // website: $website.val(), + // online_presences: op, + // performance_samples: ps + // }) + // .done(postUpdateProfileSuccess) + // .fail(postUpdateProfileFailure); + } + + function postUpdateProfileSuccess(response) { + $document.triggerHandler(EVENTS.USER_UPDATED, response); + context.location = "/client#/profile/" + context.JK.currentUserId; + } + + function postUpdateProfileFailure(xhr, textStatus, errorMessage) { + + var errors = JSON.parse(xhr.responseText) + + if(xhr.status == 422) { + + } + else { + app.ajaxError(xhr, textStatus, errorMessage) + } + } + function initialize() { var screenBindings = { 'beforeShow': beforeShow, diff --git a/web/app/assets/javascripts/profile_utils.js b/web/app/assets/javascripts/profile_utils.js index 126e60be8..edab5c367 100644 --- a/web/app/assets/javascripts/profile_utils.js +++ b/web/app/assets/javascripts/profile_utils.js @@ -17,13 +17,14 @@ var FREE_SESSION_GENRE_TYPE = 'free_session'; var COWRITING_GENRE_TYPE = 'cowriting'; - // performance samples + // performance sample types var SAMPLE_TYPES = { JAMKAZAM: {description: "jamkazam"}, SOUNDCLOUD: {description: "soundcloud"}, YOUTUBE: {description: "youtube"} }; + // online presence types var ONLINE_PRESENCE_TYPES = { SOUNDCLOUD: {description: "soundcloud"}, REVERBNATION: {description: "reverbnation"}, diff --git a/web/app/views/api_users/profile_show.rabl b/web/app/views/api_users/profile_show.rabl index 5be632329..56a822dfe 100644 --- a/web/app/views/api_users/profile_show.rabl +++ b/web/app/views/api_users/profile_show.rabl @@ -11,7 +11,7 @@ child :performance_samples => :performance_samples do attributes :id, :url, :service_type, :claimed_recording_id, :service_id, :description child :claimed_recording => :claimed_recording do - attributes :name + attributes :id, :name end end diff --git a/web/app/views/clients/_account_profile_samples.html.erb b/web/app/views/clients/_account_profile_samples.html.erb index 708e41557..51e6cd1be 100644 --- a/web/app/views/clients/_account_profile_samples.html.erb +++ b/web/app/views/clients/_account_profile_samples.html.erb @@ -19,7 +19,7 @@
- +
@@ -80,7 +80,14 @@ BROWSE
-
scrollable
+
+ + + + + +
Test RecordingX
+
@@ -91,7 +98,14 @@ ADD
-
scrollable
+
+ + + + + +
Test RecordingX
+
@@ -102,7 +116,14 @@ ADD
-
scrollable
+
+ + + + + +
Test RecordingX
+
From d3cf83db8507b32daa1cf9f013b95d48731abeb8 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 23 Mar 2015 22:26:36 -0400 Subject: [PATCH 077/195] VRFS-2701 remove use of template on experience screen --- .../accounts_profile_experience.js | 47 +++--- .../_account_profile_experience.html.erb | 134 +++++++++--------- 2 files changed, 85 insertions(+), 96 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile_experience.js b/web/app/assets/javascripts/accounts_profile_experience.js index 3e0b1efb6..53a9a2c78 100644 --- a/web/app/assets/javascripts/accounts_profile_experience.js +++ b/web/app/assets/javascripts/accounts_profile_experience.js @@ -9,14 +9,14 @@ var EVENTS = context.JK.EVENTS; var api = context.JK.Rest(); var $screen = $('#account-profile-experience'); - var $scroller = $screen.find('#account-profile-content-scroller'); - var $instrumentSelector = null; - var $userGenres = null; var profileUtils = context.JK.ProfileUtils; var $btnCancel = $screen.find('#account-edit-profile-cancel'); var $btnBack = $screen.find('#account-edit-profile-back'); var $btnSubmit = $screen.find('#account-edit-profile-submit'); + var $instrumentSelector = $screen.find('.instrument_selector'); + var $userGenres = $screen.find('#user-genres'); + function beforeShow(data) { } @@ -26,23 +26,15 @@ } function resetForm() { - $scroller.find('form .error-text').remove(); - $scroller.find('form .error').removeClass("error"); + $screen.find('form .error-text').remove(); + $screen.find('form .error').removeClass("error"); } function populateAccountProfile(userDetail, instruments) { - var template = context.JK.fillTemplate($('#template-account-profile-experience').html(), { - user_instruments: userDetail.instruments - }); - - $scroller.html(template); - - $instrumentSelector = $screen.find('.instrument_selector'); - $userGenres = $screen.find('#user-genres'); - events(); loadGenres(profileUtils.profileGenres(userDetail.genres)); + $instrumentSelector.empty(); $.each(instruments, function(index, instrument) { var template = context.JK.fillTemplate($('#account-profile-instrument').html(), { checked : isUserInstrument(instrument, userDetail.instruments) ? "checked=\"checked\"" :"", @@ -55,15 +47,15 @@ // and fill in the proficiency for the instruments that the user can play if(userDetail.instruments) { $.each(userDetail.instruments, function(index, userInstrument) { - $('tr[data-instrument-id="' + userInstrument.instrument_id + '"] select.proficiency_selector', $scroller).val(userInstrument.proficiency_level); + $('tr[data-instrument-id="' + userInstrument.instrument_id + '"] select.proficiency_selector', $screen).val(userInstrument.proficiency_level); }); } - $scroller.find('select[name=skill_level]').val(userDetail.skill_level); - $scroller.find('select[name=concert_count]').val(userDetail.concert_count); - $scroller.find('select[name=studio_session_count]').val(userDetail.studio_session_count); + $screen.find('select[name=skill_level]').val(userDetail.skill_level); + $screen.find('select[name=concert_count]').val(userDetail.concert_count); + $screen.find('select[name=studio_session_count]').val(userDetail.studio_session_count); - context.JK.dropdown($('select', $scroller)); + context.JK.dropdown($('select', $screen)); } function isUserInstrument(instrument, userInstruments) { @@ -122,6 +114,7 @@ function events() { $btnCancel.click(function(evt) { + console.log("HERE"); evt.stopPropagation(); navigateTo('/client#/profile/' + context.JK.currentUserId); return false; @@ -158,16 +151,15 @@ function handleUpdateProfile() { resetForm(); - var instruments = getInstrumentsValue(); + var instruments = getSelectedInstruments(); var genres = getSelectedGenres(); - var status = api.updateUser({ instruments: instruments, genres: genres, - skill_level: $scroller.find('select[name=skill_level]').val(), - concert_count: $scroller.find('select[name=concert_count]').val(), - studio_session_count: $scroller.find('select[name=studio_session_count]').val() + skill_level: $screen.find('select[name=skill_level]').val(), + concert_count: $screen.find('select[name=concert_count]').val(), + studio_session_count: $screen.find('select[name=studio_session_count]').val() }) .done(postUpdateProfileSuccess) .fail(postUpdateProfileFailure); @@ -186,7 +178,7 @@ var instruments = context.JK.format_errors("musician_instruments", errors); if(instruments != null) { - instrumentSelector.closest('div.field').addClass('error').append(instruments); + $instrumentSelector.closest('div.field').addClass('error').append(instruments); } } else { @@ -194,7 +186,7 @@ } } - function getInstrumentsValue() { + function getSelectedInstruments() { var instruments = []; $('input[type=checkbox]:checked', $instrumentSelector).each(function(i) { @@ -217,7 +209,10 @@ 'beforeShow': beforeShow, 'afterShow': afterShow }; + app.bindScreen('account/profile/experience', screenBindings); + + events(); } this.initialize = initialize; diff --git a/web/app/views/clients/_account_profile_experience.html.erb b/web/app/views/clients/_account_profile_experience.html.erb index 55a731ba7..5360c22b9 100644 --- a/web/app/views/clients/_account_profile_experience.html.erb +++ b/web/app/views/clients/_account_profile_experience.html.erb @@ -9,81 +9,75 @@
- + From f3344affaf0a521c6288fe82fe740f8c902fa8cf Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 26 Mar 2015 05:56:43 +0000 Subject: [PATCH 080/195] VRFS-2795 distance sort order --- ruby/lib/jam_ruby/models/musician_search.rb | 21 ++++++++++++++++--- .../models/musician_search_model_spec.rb | 16 ++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/ruby/lib/jam_ruby/models/musician_search.rb b/ruby/lib/jam_ruby/models/musician_search.rb index 395728644..69c310abd 100644 --- a/ruby/lib/jam_ruby/models/musician_search.rb +++ b/ruby/lib/jam_ruby/models/musician_search.rb @@ -183,20 +183,34 @@ module JamRuby end def _skills(rel) - if 0 < (val = json['skill_level'].to_i) + if 0 < (val = json[KEY_SKILL].to_i) rel = rel.where(['skill_level = ?', val]) end rel end def _interests(rel) - val = json['interests'] + val = json[KEY_INTERESTS] if val.present? && ANY_VAL_STR != val rel = rel.where("#{val} = ?", true) end rel end + def _sort_order(rel) + val = json[KEY_SORT_ORDER] + if SORT_VALS[1] == val + locidispid = self.user.last_jam_locidispid || 0 + my_locid = locidispid / 1000000 + rel = rel.joins("INNER JOIN geoiplocations AS my_geo ON my_geo.locid = #{my_locid}") + rel = rel.joins("INNER JOIN geoiplocations AS other_geo ON users.last_jam_locidispid/1000000 = other_geo.locid") + rel = rel.group("users.id, my_geo.geog, other_geo.geog") + rel = rel.order('st_distance(my_geo.geog, other_geo.geog)') + else + end + rel + end + def pagination(rel, params) perpage = [(params[:per_page] || PER_PAGE).to_i, 100].min page = [params[:page].to_i, 1].max @@ -204,7 +218,7 @@ module JamRuby end def do_search(params={}) - rel = User.musicians + rel = User.musicians.where(['users.id != ?', self.user.id]) rel = self._genres(rel) rel = self._ages(rel) rel = self._studios(rel) @@ -212,6 +226,7 @@ module JamRuby rel = self._skills(rel) rel = self._instruments(rel) rel = self._interests(rel) + rel = self._sort_order(rel) rel end diff --git a/ruby/spec/jam_ruby/models/musician_search_model_spec.rb b/ruby/spec/jam_ruby/models/musician_search_model_spec.rb index e6655a123..a3cb11a62 100644 --- a/ruby/spec/jam_ruby/models/musician_search_model_spec.rb +++ b/ruby/spec/jam_ruby/models/musician_search_model_spec.rb @@ -1,9 +1,16 @@ require 'spec_helper' require 'time_difference' +require 'byebug' describe 'Musician Search Model' do let!(:searcher) { FactoryGirl.create(:austin_user) } let!(:search) { MusicianSearch.create_search(searcher) } + + let!(:austin_user) { FactoryGirl.create(:austin_user) } + let!(:dallas_user) { FactoryGirl.create(:dallas_user) } + let!(:miami_user) { FactoryGirl.create(:miami_user) } + let!(:seattle_user) { FactoryGirl.create(:seattle_user) } + let!(:user_types) { [:austin_user, :dallas_user, :miami_user, :seattle_user] } describe "creates search obj" do @@ -185,5 +192,14 @@ describe 'Musician Search Model' do create_phony_database end + it "sorts by distance" do + search.update_json_value(MusicianSearch::KEY_SORT_ORDER, MusicianSearch::SORT_VALS[1]) + results = search.do_search + expect(results[0].id).to eq(austin_user.id) + expect(results[1].id).to eq(dallas_user.id) + expect(results[2].id).to eq(miami_user.id) + expect(results[3].id).to eq(seattle_user.id) + end + end end From 21322318d8ff05ee9a6427b7e88463167a793a2b Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 26 Mar 2015 06:28:38 +0000 Subject: [PATCH 081/195] VRFS-2795 latency ordering --- ruby/lib/jam_ruby/models/musician_search.rb | 2 + .../models/musician_search_model_spec.rb | 48 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/ruby/lib/jam_ruby/models/musician_search.rb b/ruby/lib/jam_ruby/models/musician_search.rb index 69c310abd..a7aa1e793 100644 --- a/ruby/lib/jam_ruby/models/musician_search.rb +++ b/ruby/lib/jam_ruby/models/musician_search.rb @@ -207,6 +207,8 @@ module JamRuby rel = rel.group("users.id, my_geo.geog, other_geo.geog") rel = rel.order('st_distance(my_geo.geog, other_geo.geog)') else + rel = rel.joins("LEFT JOIN current_scores ON current_scores.a_userid = users.id AND current_scores.b_userid = '#{self.user.id}'") + rel = rel.order('current_scores.full_score ASC') end rel end diff --git a/ruby/spec/jam_ruby/models/musician_search_model_spec.rb b/ruby/spec/jam_ruby/models/musician_search_model_spec.rb index a3cb11a62..cbb21ad3b 100644 --- a/ruby/spec/jam_ruby/models/musician_search_model_spec.rb +++ b/ruby/spec/jam_ruby/models/musician_search_model_spec.rb @@ -1,6 +1,5 @@ require 'spec_helper' require 'time_difference' -require 'byebug' describe 'Musician Search Model' do let!(:searcher) { FactoryGirl.create(:austin_user) } @@ -187,7 +186,7 @@ describe 'Musician Search Model' do end end - describe "sort order" do + describe "sort order by distance" do before(:each) do create_phony_database end @@ -200,6 +199,51 @@ describe 'Musician Search Model' do expect(results[2].id).to eq(miami_user.id) expect(results[3].id).to eq(seattle_user.id) end + end + + describe "sort order by latency" do + before(:each) do + User.delete_all + t = Time.now - 10.minute + + @user1 = FactoryGirl.create(:user, created_at: t+1.minute, last_jam_locidispid: 1) + @user2 = FactoryGirl.create(:user, created_at: t+2.minute, last_jam_locidispid: 2) + @user3 = FactoryGirl.create(:user, created_at: t+3.minute, last_jam_locidispid: 3) + @user4 = FactoryGirl.create(:user, created_at: t+4.minute, last_jam_locidispid: 4) + @user5 = FactoryGirl.create(:user, created_at: t+5.minute, last_jam_locidispid: 5) + @user6 = FactoryGirl.create(:user, created_at: t+6.minute) # not geocoded + @user7 = FactoryGirl.create(:user, created_at: t+7.minute, musician: false) # not musician + + @musicians = [] + @musicians << @user1 + @musicians << @user2 + @musicians << @user3 + @musicians << @user4 +< @musicians << @user5 + @musicians << @user6 + + # from these scores: + # user1 has scores other users in user1 location, and with user2, user3, user4 + # user2 has scores with users in user3 and user4 location + # Score.delete_all + # Score.connection.execute('delete from current_network_scores').check + Score.createx(1, 'a', 1, 1, 'a', 1, 10) + Score.createx(1, 'a', 1, 2, 'b', 2, 20) + Score.createx(1, 'a', 1, 3, 'c', 3, 30) + Score.createx(1, 'a', 1, 4, 'd', 4, 40) + Score.createx(2, 'b', 2, 3, 'c', 3, 15) + Score.createx(2, 'b', 2, 4, 'd', 4, 70) + end + + it "sorts by latency" do + search.update_json_value(MusicianSearch::KEY_SORT_ORDER, MusicianSearch::SORT_VALS[0]) + results = search.do_search + expect(results[0].id).to eq(@user1.id) + expect(results[1].id).to eq(@user2.id) + expect(results[2].id).to eq(@user3.id) + expect(results[3].id).to eq(@user4.id) + end end + end From a0e9dbdd25d61b5b459c1ecd7cc76c7986f1e8a6 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sun, 29 Mar 2015 10:06:39 -0400 Subject: [PATCH 082/195] VRFS-2701 added callbacks to site validator to highlight invalid usernames and site URLs --- .../javascripts/accounts_profile_samples.js | 25 ++++++- .../javascripts/site_validator.js.coffee | 13 +++- .../client/accountProfileSamples.css.scss | 4 + .../client/site_validator.css.scss | 2 +- .../clients/_account_profile_samples.html.erb | 74 ++++++++++++------- 5 files changed, 84 insertions(+), 34 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 0b5f36b8d..8e96446b4 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -168,13 +168,32 @@ }); $btnSubmit.click(function(evt) { - $document.triggerHandler(EVENTS.USER_UPDATED, response); - - handleUpdateProfile(); + if (validate()) { + handleUpdateProfile(); + } return false; }); } + function validate() { + // website + if ($.trim($website.val()).length > 0) { + + } + + // SoundCloud + if ($.trim($soundCloudUsername.val()).length > 0) { + + } + + // ReverbNation + if ($.trim($reverbNationUsername.val())) { + + } + + return true; + } + function navigateTo(targetLocation) { context.location = targetLocation; } diff --git a/web/app/assets/javascripts/site_validator.js.coffee b/web/app/assets/javascripts/site_validator.js.coffee index 2c63c7ed7..588c608fe 100644 --- a/web/app/assets/javascripts/site_validator.js.coffee +++ b/web/app/assets/javascripts/site_validator.js.coffee @@ -4,7 +4,7 @@ context.JK ||= {}; context.JK.SiteValidator = class SiteValidator - constructor: (site_type) -> + constructor: (site_type, success_callback, fail_callback) -> @EVENTS = context.JK.EVENTS @rest = context.JK.Rest() @site_type = site_type @@ -18,6 +18,8 @@ context.JK.SiteValidator = class SiteValidator @is_rec_src = false @deferred_status_check = null @is_validating = false + @success_callback = success_callback + @fail_callback = fail_callback init: () => this.renderErrors({}) @@ -68,11 +70,16 @@ context.JK.SiteValidator = class SiteValidator this.renderErrors({}) if @deferred_status_check @deferred_status_check.resolve() + if @success_callback + @success_callback(@input_div) else this.setSiteStatus(false) this.renderErrors(response) if @deferred_status_check @deferred_status_check.reject() + if @fail_callback + @fail_callback(@input_div) + @deferred_status_check = null @logger.debug("site_status = "+@site_status) @@ -120,8 +127,8 @@ context.JK.SiteValidator = class SiteValidator context.JK.RecordingSourceValidator = class RecordingSourceValidator extends SiteValidator - constructor: (site_type) -> - super(site_type) + constructor: (site_type, success_callback, fail_callback) -> + super(site_type, success_callback, fail_callback) @recording_sources = [] @is_rec_src = true @add_btn = @input_div.find('a.add-recording-source') diff --git a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss index 1e0b4caa1..e5f1e310b 100644 --- a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss +++ b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss @@ -25,6 +25,10 @@ .sample { margin: 3px 5px 10px 0px; + + a.add-recording-source { + margin-top: 2px !important; + } } .sample-list { diff --git a/web/app/assets/stylesheets/client/site_validator.css.scss b/web/app/assets/stylesheets/client/site_validator.css.scss index 40f3ac22f..09853cdae 100644 --- a/web/app/assets/stylesheets/client/site_validator.css.scss +++ b/web/app/assets/stylesheets/client/site_validator.css.scss @@ -24,7 +24,7 @@ display:inline-block; vertical-align: middle; position: relative; - margin-top: -40px; + margin-top: -10px; left: 0px; } .error { diff --git a/web/app/views/clients/_account_profile_samples.html.erb b/web/app/views/clients/_account_profile_samples.html.erb index 959266f59..79e63f5ae 100644 --- a/web/app/views/clients/_account_profile_samples.html.erb +++ b/web/app/views/clients/_account_profile_samples.html.erb @@ -19,25 +19,25 @@
- +
- +
- +
- +
@@ -46,25 +46,25 @@
- +
- +
- +
- +
@@ -92,10 +92,9 @@
- -
-
- ADD + + ADD +
@@ -110,10 +109,9 @@
- -
-
- ADD + + ADD +
@@ -155,36 +153,58 @@ initialized = true; setTimeout(function() { - window.urlValidator = new JK.SiteValidator('url'); + window.urlValidator = new JK.SiteValidator('url', userNameSuccessCallback, userNameFailCallback); urlValidator.init(); - window.soundCloudValidator = new JK.SiteValidator('soundcloud'); + window.soundCloudValidator = new JK.SiteValidator('soundcloud', userNameSuccessCallback, userNameFailCallback); soundCloudValidator.init(); - window.reverbNationValidator = new JK.SiteValidator('reverbnation'); + window.reverbNationValidator = new JK.SiteValidator('reverbnation', userNameSuccessCallback, userNameFailCallback); reverbNationValidator.init(); - window.bandCampValidator = new JK.SiteValidator('bandcamp'); + window.bandCampValidator = new JK.SiteValidator('bandcamp', userNameSuccessCallback, userNameFailCallback); bandCampValidator.init(); - window.fandalismValidator = new JK.SiteValidator('fandalism'); + window.fandalismValidator = new JK.SiteValidator('fandalism', userNameSuccessCallback, userNameFailCallback); fandalismValidator.init(); - window.youTubeValidator = new JK.SiteValidator('youtube'); + window.youTubeValidator = new JK.SiteValidator('youtube', userNameSuccessCallback, userNameFailCallback); youTubeValidator.init(); - window.facebookValidator = new JK.SiteValidator('facebook'); + window.facebookValidator = new JK.SiteValidator('facebook', userNameSuccessCallback, userNameFailCallback); facebookValidator.init(); - window.twitterValidator = new JK.SiteValidator('twitter'); + window.twitterValidator = new JK.SiteValidator('twitter', userNameSuccessCallback, userNameFailCallback); twitterValidator.init(); - window.soundCloudRecordingValidator = new JK.SiteValidator('rec_soundcloud'); + window.soundCloudRecordingValidator = new JK.SiteValidator('rec_soundcloud', userNameSuccessCallback, userNameFailCallback); soundCloudRecordingValidator.init(); - window.youTubeRecordingValidator = new JK.SiteValidator('rec_youtube'); + window.youTubeRecordingValidator = new JK.SiteValidator('rec_youtube', userNameSuccessCallback, userNameFailCallback); youTubeRecordingValidator.init(); - }, 1) + }, 1); + + function userNameSuccessCallback($inputDiv) { + $inputDiv.removeClass('error'); + $inputDiv.find('.error-text').remove(); + } + + function userNameFailCallback($inputDiv) { + $inputDiv.addClass('error'); + $inputDiv.find('.error-text').remove(); + $inputDiv.append("Invalid username").show(); + } + + function siteSuccessCallback($inputDiv) { + $inputDiv.removeClass('error'); + $inputDiv.find('.error-text').remove(); + } + + function siteFailCallback($inputDiv) { + $inputDiv.addClass('error'); + $inputDiv.find('.error-text').remove(); + $inputDiv.append("Invalid URL").show(); + } }); }); From b02013846e1f5c3e1e48cd13ae07decd704f0ddf Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sun, 29 Mar 2015 21:22:12 -0400 Subject: [PATCH 083/195] VRFS-2701 remove error message when username is blank --- web/app/assets/javascripts/site_validator.js.coffee | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/app/assets/javascripts/site_validator.js.coffee b/web/app/assets/javascripts/site_validator.js.coffee index 588c608fe..fc54465dd 100644 --- a/web/app/assets/javascripts/site_validator.js.coffee +++ b/web/app/assets/javascripts/site_validator.js.coffee @@ -55,6 +55,8 @@ context.JK.SiteValidator = class SiteValidator validateSite: () => unless data = this.dataToValidate() + if @success_callback + @success_callback(@input_div) return null this.setSiteStatus(null) @spinner.show() From 546205b18669b4c6c2a4b77b516297e0e1b03879 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Tue, 31 Mar 2015 20:35:38 -0400 Subject: [PATCH 084/195] VRFS-2701 wip editing presence and samples --- .../assets/javascripts/accounts_profile.js | 56 +++--- .../javascripts/accounts_profile_samples.js | 44 +---- web/app/assets/javascripts/profile_utils.js | 24 +-- .../client/accountProfileSamples.css.scss | 3 +- web/app/assets/stylesheets/client/client.css | 1 - .../views/clients/_account_profile.html.erb | 171 +++++++++--------- 6 files changed, 137 insertions(+), 162 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile.js b/web/app/assets/javascripts/accounts_profile.js index b7189774a..1572d55a3 100644 --- a/web/app/assets/javascripts/accounts_profile.js +++ b/web/app/assets/javascripts/accounts_profile.js @@ -18,11 +18,19 @@ var nilOptionStr = ''; var nilOptionText = 'n/a'; var $screen = $('#account-profile-basics'); + var $avatar = $screen.find('#avatar'); + var $country = $screen.find('#country'); + var $region = $screen.find('#region'); + var $city = $screen.find('#city'); + var $firstName = $screen.find('#first-name'); + var $lastName = $screen.find('#last-name'); + var $gender = $screen.find('#gender'); + var $biography = $screen.find('#biography'); + var $subscribe = $screen.find('#subscribe'); + var $btnCancel = $screen.find('#account-edit-profile-cancel'); var $btnSubmit = $screen.find('#account-edit-profile-submit'); - var $biography = null; - function beforeShow(data) { userId = data.id; } @@ -39,27 +47,33 @@ } function populateAccountProfile(userDetail) { - var template = context.JK.fillTemplate($('#template-account-profile-basics').html(), { - country: userDetail.country, - region: userDetail.state, - city: userDetail.city, - first_name: userDetail.first_name, - last_name: userDetail.last_name, - photoUrl: context.JK.resolveAvatarUrl(userDetail.photo_url), - birth_date : userDetail.birth_date, - gender: userDetail.gender, - biography: userDetail.biography ? userDetail.biography : '', - subscribe_email: userDetail.subscribe_email ? "checked=checked" : "" - }); + // var template = context.JK.fillTemplate($('#template-account-profile-basics').html(), { + // country: userDetail.country, + // region: userDetail.state, + // city: userDetail.city, + // first_name: userDetail.first_name, + // last_name: userDetail.last_name, + // photoUrl: context.JK.resolveAvatarUrl(userDetail.photo_url), + // birth_date : userDetail.birth_date, + // gender: userDetail.gender, + // biography: userDetail.biography ? userDetail.biography : '', + // subscribe_email: userDetail.subscribe_email ? "checked=checked" : "" + // }); + + $avatar.attr('src', context.JK.resolveAvatarUrl(userDetail.photo_url)); + $country.val(userDetail.country); + $region.val(userDetail.state); + $city.val(userDetail.city); + $firstName.val(userDetail.first_name); + $lastName.val(userDetail.last_name); + $gender.val(userDetail.gender); + $biography.val(userDetail.biography); + + if (userDetail.subscribe_email) { + $subscribe.attr('checked', 'checked'); + } var content_root = $('#account-profile-content-scroller'); - content_root.html(template); - - $biography = $screen.find('#biography'); - - // now use javascript to fix up values too hard to do with templating - // set gender - $('select[name=gender]', content_root).val(userDetail.gender) // set birth_date if(userDetail.birth_date) { diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 8e96446b4..97a83ed51 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -137,24 +137,6 @@ return false; }); - $btnAddSoundCloudRecording.click(function(evt) { - var url = $soundCloudRecordingUrl.val(); - if (url.length > 0) { - if (extractSoundCloudUrlParts(url)) { - // add to list - } - } - }); - - $btnAddYouTubeVideo.click(function(evt) { - var url = $youTubeVideoUrl.val(); - if (url.length) { - if (extractYouTubeUrlParts(url)) { - // add to list - } - } - }); - $btnCancel.click(function(evt) { evt.stopPropagation(); navigateTo('/client#/profile/' + context.JK.currentUserId); @@ -176,22 +158,8 @@ } function validate() { - // website - if ($.trim($website.val()).length > 0) { - - } - - // SoundCloud - if ($.trim($soundCloudUsername.val()).length > 0) { - - } - - // ReverbNation - if ($.trim($reverbNationUsername.val())) { - - } - - return true; + var errors = $screen.find('.site_validator.error'); + return !(errors && errors.length > 0); } function navigateTo(targetLocation) { @@ -199,7 +167,7 @@ } function addOnlinePresence(presenceArray, username, type) { - if ($.trim($soundCloudUsername.val()).length > 0) { + if ($.trim(username).length > 0) { presenceArray.push({ service_type: type, username: username @@ -228,9 +196,9 @@ // extract performance samples var ps = []; var performanceSampleTypes = profileUtils.SAMPLE_TYPES; - addPerformanceSamples(ps, $jamkazamSampleList, performanceSampleTypes.JAMKAZAM); - addPerformanceSamples(ps, $soundCloudSampleList, performanceSampleTypes.SOUNDCLOUD); - addPerformanceSamples(ps, $youTubeSampleList, performanceSampleTypes.YOUTUBE); + addPerformanceSamples(ps, $jamkazamSampleList, performanceSampleTypes.JAMKAZAM.description); + addPerformanceSamples(ps, $soundCloudSampleList, performanceSampleTypes.SOUNDCLOUD.description); + addPerformanceSamples(ps, $youTubeSampleList, performanceSampleTypes.YOUTUBE.description); // api.updateUser({ // website: $website.val(), diff --git a/web/app/assets/javascripts/profile_utils.js b/web/app/assets/javascripts/profile_utils.js index edab5c367..79c12c2fc 100644 --- a/web/app/assets/javascripts/profile_utils.js +++ b/web/app/assets/javascripts/profile_utils.js @@ -18,14 +18,14 @@ var COWRITING_GENRE_TYPE = 'cowriting'; // performance sample types - var SAMPLE_TYPES = { + profileUtils.SAMPLE_TYPES = { JAMKAZAM: {description: "jamkazam"}, SOUNDCLOUD: {description: "soundcloud"}, YOUTUBE: {description: "youtube"} }; // online presence types - var ONLINE_PRESENCE_TYPES = { + profileUtils.ONLINE_PRESENCE_TYPES = { SOUNDCLOUD: {description: "soundcloud"}, REVERBNATION: {description: "reverbnation"}, BANDCAMP: {description: "bandcamp"}, @@ -176,7 +176,7 @@ profileUtils.jamkazamSamples = function(samples) { var matches = $.grep(samples, function(s) { - return s.service_type === SAMPLE_TYPES.JAMKAZAM.description; + return s.service_type === profileUtils.SAMPLE_TYPES.JAMKAZAM.description; }); return matches; @@ -184,7 +184,7 @@ profileUtils.soundCloudSamples = function(samples) { var matches = $.grep(samples, function(s) { - return s.service_type === SAMPLE_TYPES.SOUNDCLOUD.description; + return s.service_type === profileUtils.SAMPLE_TYPES.SOUNDCLOUD.description; }); return matches; @@ -192,7 +192,7 @@ profileUtils.youTubeSamples = function(samples) { var matches = $.grep(samples, function(s) { - return s.service_type === SAMPLE_TYPES.YOUTUBE.description; + return s.service_type === profileUtils.SAMPLE_TYPES.YOUTUBE.description; }); return matches; @@ -200,7 +200,7 @@ profileUtils.soundCloudPresences = function(presences) { var matches = $.grep(presences, function(p) { - return p.service_type === ONLINE_PRESENCE_TYPES.SOUNDCLOUD.description; + return p.service_type === profileUtils.ONLINE_PRESENCE_TYPES.SOUNDCLOUD.description; }); return matches; @@ -208,7 +208,7 @@ profileUtils.reverbNationPresences = function(presences) { var matches = $.grep(presences, function(p) { - return p.service_type === ONLINE_PRESENCE_TYPES.REVERBNATION.description; + return p.service_type === profileUtils.ONLINE_PRESENCE_TYPES.REVERBNATION.description; }); return matches; @@ -216,7 +216,7 @@ profileUtils.bandCampPresences = function(presences) { var matches = $.grep(presences, function(p) { - return p.service_type === ONLINE_PRESENCE_TYPES.BANDCAMP.description; + return p.service_type === profileUtils.ONLINE_PRESENCE_TYPES.BANDCAMP.description; }); return matches; @@ -224,7 +224,7 @@ profileUtils.fandalismPresences = function(presences) { var matches = $.grep(presences, function(p) { - return p.service_type === ONLINE_PRESENCE_TYPES.FANDALISM.description; + return p.service_type === profileUtils.ONLINE_PRESENCE_TYPES.FANDALISM.description; }); return matches; @@ -232,7 +232,7 @@ profileUtils.youTubePresences = function(presences) { var matches = $.grep(presences, function(p) { - return p.service_type === ONLINE_PRESENCE_TYPES.YOUTUBE.description; + return p.service_type === profileUtils.ONLINE_PRESENCE_TYPES.YOUTUBE.description; }); return matches; @@ -240,7 +240,7 @@ profileUtils.facebookPresences = function(presences) { var matches = $.grep(presences, function(p) { - return p.service_type === ONLINE_PRESENCE_TYPES.FACEBOOK.description; + return p.service_type === profileUtils.ONLINE_PRESENCE_TYPES.FACEBOOK.description; }); return matches; @@ -248,7 +248,7 @@ profileUtils.twitterPresences = function(presences) { var matches = $.grep(presences, function(p) { - return p.service_type === ONLINE_PRESENCE_TYPES.TWITTER.description; + return p.service_type === profileUtils.ONLINE_PRESENCE_TYPES.TWITTER.description; }); return matches; diff --git a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss index e5f1e310b..dfe7bc128 100644 --- a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss +++ b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss @@ -32,7 +32,8 @@ } .sample-list { - + height: inherit; + overflow: auto; } } } \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index 28049797e..c48ea6bef 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -59,7 +59,6 @@ *= require ./checkout_signin *= require ./checkout_payment *= require ./checkout_order - *= require ./genreSelector *= require ./sessionList *= require ./searchResults *= require ./clientUpdate diff --git a/web/app/views/clients/_account_profile.html.erb b/web/app/views/clients/_account_profile.html.erb index 08312237b..7d6da9224 100644 --- a/web/app/views/clients/_account_profile.html.erb +++ b/web/app/views/clients/_account_profile.html.erb @@ -1,105 +1,98 @@
- +
- +
<%= image_tag "content/icon_account.png", {:width => 27, :height => 20} %>
- +

my account

<%= render "screen_navigation" %>
- -
-
- - From 9d4943d81400e183ee34081227c0e4dd26133b48 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Tue, 31 Mar 2015 20:42:57 -0400 Subject: [PATCH 085/195] VRFS-2701 fixed HTML --- web/app/assets/javascripts/accounts_profile.js | 12 ------------ web/app/views/clients/_account_profile.html.erb | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile.js b/web/app/assets/javascripts/accounts_profile.js index 1572d55a3..c791fb567 100644 --- a/web/app/assets/javascripts/accounts_profile.js +++ b/web/app/assets/javascripts/accounts_profile.js @@ -47,18 +47,6 @@ } function populateAccountProfile(userDetail) { - // var template = context.JK.fillTemplate($('#template-account-profile-basics').html(), { - // country: userDetail.country, - // region: userDetail.state, - // city: userDetail.city, - // first_name: userDetail.first_name, - // last_name: userDetail.last_name, - // photoUrl: context.JK.resolveAvatarUrl(userDetail.photo_url), - // birth_date : userDetail.birth_date, - // gender: userDetail.gender, - // biography: userDetail.biography ? userDetail.biography : '', - // subscribe_email: userDetail.subscribe_email ? "checked=checked" : "" - // }); $avatar.attr('src', context.JK.resolveAvatarUrl(userDetail.photo_url)); $country.val(userDetail.country); diff --git a/web/app/views/clients/_account_profile.html.erb b/web/app/views/clients/_account_profile.html.erb index 7d6da9224..a0f3cde6f 100644 --- a/web/app/views/clients/_account_profile.html.erb +++ b/web/app/views/clients/_account_profile.html.erb @@ -89,7 +89,7 @@
-
+
From 03f94480e76c8ac90bd403ee25b0e0b2aedafcfa Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Tue, 31 Mar 2015 22:32:41 -0400 Subject: [PATCH 086/195] VRFS-2701 wip adding recording sources --- .../javascripts/accounts_profile_samples.js | 28 ++++----- .../javascripts/site_validator.js.coffee | 3 + .../client/accountProfileSamples.css.scss | 6 +- .../clients/_account_profile_samples.html.erb | 58 +++++++++++-------- 4 files changed, 53 insertions(+), 42 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 97a83ed51..e884d3549 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -25,18 +25,12 @@ var $twitterUsername = $screen.find('#twitter-username'); // performance samples - var $soundCloudRecordingUrl = $screen.find('#soundcloud-recording'); - var $youTubeVideoUrl = $screen.find('#youtube-video'); - var $jamkazamSampleList = $screen.find('.samples.jamkazam'); var $soundCloudSampleList = $screen.find('.samples.soundcloud'); var $youTubeSampleList = $screen.find('.samples.youtube'); // buttons var $btnAddJkRecording = $screen.find('#btn-add-jk-recording'); - var $btnAddSoundCloudRecording = $screen.find('#btn-add-soundcloud-recording'); - var $btnAddYouTubeVideo = $screen.find('#btn-add-youtube-video'); - var $btnCancel = $screen.find('#account-edit-profile-cancel'); var $btnBack = $screen.find('#account-edit-profile-back'); var $btnSubmit = $screen.find('#account-edit-profile-submit'); @@ -102,7 +96,7 @@ // JamKazam recordings var samples = profileUtils.jamkazamSamples(user.performance_samples); - if (samples) { + if (samples && samples.length > 0) { $.each(samples, function(index, val) { }); @@ -110,7 +104,7 @@ // SoundCloud recordings samples = profileUtils.soundCloudSamples(user.performance_samples); - if (samples) { + if (samples && samples.length > 0) { $.each(samples, function(index, val) { }); @@ -118,9 +112,9 @@ // YouTube videos samples = profileUtils.youTubeSamples(user.performance_samples); - if (samples) { + if (samples && samples.length > 0) { $.each(samples, function(index, val) { - + }); } } @@ -200,13 +194,13 @@ addPerformanceSamples(ps, $soundCloudSampleList, performanceSampleTypes.SOUNDCLOUD.description); addPerformanceSamples(ps, $youTubeSampleList, performanceSampleTypes.YOUTUBE.description); - // api.updateUser({ - // website: $website.val(), - // online_presences: op, - // performance_samples: ps - // }) - // .done(postUpdateProfileSuccess) - // .fail(postUpdateProfileFailure); + api.updateUser({ + website: $website.val(), + online_presences: op, + performance_samples: ps + }) + .done(postUpdateProfileSuccess) + .fail(postUpdateProfileFailure); } function postUpdateProfileSuccess(response) { diff --git a/web/app/assets/javascripts/site_validator.js.coffee b/web/app/assets/javascripts/site_validator.js.coffee index fc54465dd..a17bbd565 100644 --- a/web/app/assets/javascripts/site_validator.js.coffee +++ b/web/app/assets/javascripts/site_validator.js.coffee @@ -174,3 +174,6 @@ context.JK.RecordingSourceValidator = class RecordingSourceValidator extends Sit src_data['url'] == url 0 < vals.length + recordingSources: () => + @recording_sources + diff --git a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss index dfe7bc128..11b3bb704 100644 --- a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss +++ b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss @@ -31,8 +31,10 @@ } } - .sample-list { - height: inherit; + div.sample-list { + height: 250px; + width: 250px; + border: 2px solid #ccc; overflow: auto; } } diff --git a/web/app/views/clients/_account_profile_samples.html.erb b/web/app/views/clients/_account_profile_samples.html.erb index 79e63f5ae..35c61d4ee 100644 --- a/web/app/views/clients/_account_profile_samples.html.erb +++ b/web/app/views/clients/_account_profile_samples.html.erb @@ -74,21 +74,18 @@
+
-
+

- - - - - -
Test RecordingX
+  
+
@@ -98,14 +95,10 @@
- - - - - -
Test RecordingX
+  
+
@@ -115,22 +108,18 @@
- - - - - -
Test RecordingX
+  
+


- CANCEL   - BACK   + CANCEL  + BACK  SAVE & FINISH
@@ -152,6 +141,12 @@ } initialized = true; + var $screen = $('#account-profile-samples'); + var $btnAddSoundCloudRecording = $screen.find('#btn-add-soundcloud-recording'); + var $btnAddYouTubeVideo = $screen.find('#btn-add-youtube-video'); + var $soundCloudSampleList = $screen.find('.samples.soundcloud'); + var $youTubeSampleList = $screen.find('.samples.youtube'); + setTimeout(function() { window.urlValidator = new JK.SiteValidator('url', userNameSuccessCallback, userNameFailCallback); urlValidator.init(); @@ -177,13 +172,30 @@ window.twitterValidator = new JK.SiteValidator('twitter', userNameSuccessCallback, userNameFailCallback); twitterValidator.init(); - window.soundCloudRecordingValidator = new JK.SiteValidator('rec_soundcloud', userNameSuccessCallback, userNameFailCallback); + window.soundCloudRecordingValidator = new JK.SiteValidator('rec_soundcloud', siteSuccessCallback, siteFailCallback); soundCloudRecordingValidator.init(); - window.youTubeRecordingValidator = new JK.SiteValidator('rec_youtube', userNameSuccessCallback, userNameFailCallback); + window.youTubeRecordingValidator = new JK.SiteValidator('rec_youtube', siteSuccessCallback, siteFailCallback); youTubeRecordingValidator.init(); }, 1); + $btnAddSoundCloudRecording.click(function(evt) { + evt.preventDefault(); + console.log("IN ADD"); + if (!$('#rec_soundcloud_validator').hasClass('error')) { + console.log("NO ERROR"); + var recordingSources = [];//window.soundCloudRecordingValidator.recordingSources(); + console.log("recordingSources=%o", recordingSources); + if (recordingSources && recordingSources.length > 0) { + var $sampleList = $soundCloudSampleList.find('.sample-list'); + var addedRecording = recordingSources[recordingSources.length-1]; + $sampleList.append('
'); + $sampleList.append(addedRecording.url); + $sampleList.append('
'); + } + } + }); + function userNameSuccessCallback($inputDiv) { $inputDiv.removeClass('error'); $inputDiv.find('.error-text').remove(); From 269519a394a6385a19eeda6567898029756cad4b Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Wed, 1 Apr 2015 23:09:16 -0400 Subject: [PATCH 087/195] VRFS-2701 wip adding external recordings --- .../javascripts/site_validator.js.coffee | 14 ++++++-- .../clients/_account_profile_samples.html.erb | 33 ++++++++----------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/web/app/assets/javascripts/site_validator.js.coffee b/web/app/assets/javascripts/site_validator.js.coffee index a17bbd565..0c450cd72 100644 --- a/web/app/assets/javascripts/site_validator.js.coffee +++ b/web/app/assets/javascripts/site_validator.js.coffee @@ -83,7 +83,7 @@ context.JK.SiteValidator = class SiteValidator @fail_callback(@input_div) @deferred_status_check = null - @logger.debug("site_status = "+@site_status) + @logger.debug("site_status = " + @site_status) processSiteCheckFail: (response) => @logger.error("site check error") @@ -130,10 +130,12 @@ context.JK.SiteValidator = class SiteValidator context.JK.RecordingSourceValidator = class RecordingSourceValidator extends SiteValidator constructor: (site_type, success_callback, fail_callback) -> - super(site_type, success_callback, fail_callback) + super(site_type) @recording_sources = [] @is_rec_src = true @add_btn = @input_div.find('a.add-recording-source') + @site_success_callback = success_callback + @site_fail_callback = fail_callback init: (sources) => super() @@ -145,11 +147,17 @@ context.JK.RecordingSourceValidator = class RecordingSourceValidator extends Sit processSiteCheckSucceed: (response) => super(response) @add_btn.removeClass('disabled') - @recording_sources.push({ url: response.data, recording_id: response.recording_id }) + + if @site_status + @recording_sources.push({ url: response.data, recording_id: response.recording_id }) + if @site_success_callback + @site_success_callback(@input_div) processSiteCheckFail: (response) => super(response) @add_btn.removeClass('disabled') + if @site_fail_callback + @site_fail_callback(@input_div) didBlur: () => # do nothing, validate on add only diff --git a/web/app/views/clients/_account_profile_samples.html.erb b/web/app/views/clients/_account_profile_samples.html.erb index 35c61d4ee..e295830e0 100644 --- a/web/app/views/clients/_account_profile_samples.html.erb +++ b/web/app/views/clients/_account_profile_samples.html.erb @@ -172,30 +172,13 @@ window.twitterValidator = new JK.SiteValidator('twitter', userNameSuccessCallback, userNameFailCallback); twitterValidator.init(); - window.soundCloudRecordingValidator = new JK.SiteValidator('rec_soundcloud', siteSuccessCallback, siteFailCallback); + window.soundCloudRecordingValidator = new JK.RecordingSourceValidator('rec_soundcloud', siteSuccessCallback, siteFailCallback); soundCloudRecordingValidator.init(); - window.youTubeRecordingValidator = new JK.SiteValidator('rec_youtube', siteSuccessCallback, siteFailCallback); + window.youTubeRecordingValidator = new JK.RecordingSourceValidator('rec_youtube', siteSuccessCallback, siteFailCallback); youTubeRecordingValidator.init(); }, 1); - $btnAddSoundCloudRecording.click(function(evt) { - evt.preventDefault(); - console.log("IN ADD"); - if (!$('#rec_soundcloud_validator').hasClass('error')) { - console.log("NO ERROR"); - var recordingSources = [];//window.soundCloudRecordingValidator.recordingSources(); - console.log("recordingSources=%o", recordingSources); - if (recordingSources && recordingSources.length > 0) { - var $sampleList = $soundCloudSampleList.find('.sample-list'); - var addedRecording = recordingSources[recordingSources.length-1]; - $sampleList.append('
'); - $sampleList.append(addedRecording.url); - $sampleList.append('
'); - } - } - }); - function userNameSuccessCallback($inputDiv) { $inputDiv.removeClass('error'); $inputDiv.find('.error-text').remove(); @@ -210,6 +193,18 @@ function siteSuccessCallback($inputDiv) { $inputDiv.removeClass('error'); $inputDiv.find('.error-text').remove(); + + var recordingSources = window.soundCloudRecordingValidator.recordingSources(); + if (recordingSources && recordingSources.length > 0) { + console.log('recordingSources=%o', recordingSources); + var $sampleList = $soundCloudSampleList.find('.sample-list'); + var addedRecording = recordingSources[recordingSources.length-1]; + $sampleList.append('
'); + $sampleList.append(addedRecording.url); + $sampleList.append('
'); + } + + $inputDiv.find('input').val(''); } function siteFailCallback($inputDiv) { From 0ccf30b023df33adda51d213cb156a9af1f2b0d3 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 2 Apr 2015 12:27:20 +0000 Subject: [PATCH 088/195] VRFS-2795 adding result rows --- .../musician_search_filter.js.coffee | 92 ++++++++++++++++++- web/app/assets/stylesheets/client/client.css | 1 - .../clients/_musician_search_filter.html.slim | 58 +++++++++++- 3 files changed, 147 insertions(+), 4 deletions(-) diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index 4410b2548..2d7e89fb1 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -173,6 +173,12 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter vals.push $(this).val() vals + didSearch: (response) => + console.log("response = ", JSON.stringify(response)) + $('#musician-search-filter-builder-form').hide() + $('#musician-search-filter-builder-results').show() + this.renderMusicians(response) + performSearch: () => $.each gon.musician_search_meta.filter_keys.single, (index, key) => @searchFilter[key] = this._selectedValue(key) @@ -180,5 +186,87 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter $.each gon.musician_search_meta.filter_keys.multi, (index, key) => @searchFilter[key] = this._selectedMultiValue(key) - @rest.postMusicianSearchFilter { filter: JSON.stringify(@searchFilter), page: 1 } - \ No newline at end of file + @rest.postMusicianSearchFilter({ filter: JSON.stringify(@searchFilter), page: 1 }).done(this.didSearch) + + renderMusicians: (musicianList) => + ii = undefined + len = undefined + mTemplate = $('#template-search-musician-row').html() + aTemplate = $('#template-search-musician-action-btns').html() + mVals = undefined + musician = undefined + renderings = '' + instr_logos = undefined + instr = undefined + follows = undefined + followVals = undefined + aFollow = undefined + myAudioLatency = musicianList.my_audio_latency + ii = 0 + len = musicians.length + while ii < len + musician = musicians[ii] + if context.JK.currentUserId == musician.id + ii++ + continue + instr_logos = '' + jj = 0 + ilen = musician['instruments'].length + while jj < ilen + toolTip = '' + if musician['instruments'][jj].instrument_id in instrument_logo_map + instr = instrument_logo_map[musician['instruments'][jj].instrument_id].asset + toolTip = musician['instruments'][jj].instrument_id + instr_logos += '' + jj++ + actionVals = + profile_url: '/client#/profile/' + musician.id + friend_class: 'button-' + (if musician['is_friend'] then 'grey' else 'orange') + friend_caption: (if musician.is_friend then 'DIS' else '') + 'CONNECT' + follow_class: 'button-' + (if musician['is_following'] then 'grey' else 'orange') + follow_caption: (if musician.is_following then 'UN' else '') + 'FOLLOW' + message_class: 'button-orange' + message_caption: 'MESSAGE' + button_message: 'button-orange' + musician_actions = context.JK.fillTemplate(aTemplate, actionVals) + latencyBadge = context._.template($templateAccountSessionLatency.html(), $.extend(sessionUtils.createLatency(musician), musician), variable: 'data') + mVals = + avatar_url: context.JK.resolveAvatarUrl(musician.photo_url) + profile_url: '/client#/profile/' + musician.id + musician_name: musician.name + musician_location: formatLocation(musician) + instruments: instr_logos + biography: musician['biography'] + follow_count: musician['follow_count'] + friend_count: musician['friend_count'] + recording_count: musician['recording_count'] + session_count: musician['session_count'] + musician_id: musician['id'] + musician_action_template: musician_actions + latency_badge: latencyBadge + musician_first_name: musician['first_name'] + $rendering = $(context.JK.fillTemplate(mTemplate, mVals)) + $offsetParent = $results.closest('.content') + data = entity_type: 'musician' + options = + positions: [ + 'top' + 'bottom' + 'right' + 'left' + ] + offsetParent: $offsetParent + scoreOptions = offsetParent: $offsetParent + context.JK.helpBubble $('.follower-count', $rendering), 'follower-count', data, options + context.JK.helpBubble $('.friend-count', $rendering), 'friend-count', data, options + context.JK.helpBubble $('.recording-count', $rendering), 'recording-count', data, options + context.JK.helpBubble $('.session-count', $rendering), 'session-count', data, options + helpBubble.scoreBreakdown $('.latency', $rendering), false, musician['full_score'], myAudioLatency, musician['audio_latency'], musician['score'], scoreOptions + $results.append $rendering + $rendering.find('.biography').dotdotdot() + ii++ + $('.search-m-friend').on 'click', friendMusician + $('.search-m-follow').on 'click', followMusician + $('.search-m-message').on 'click', messageMusician + context.JK.bindHoverEvents() + return \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index 28049797e..c48ea6bef 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -59,7 +59,6 @@ *= require ./checkout_signin *= require ./checkout_payment *= require ./checkout_order - *= require ./genreSelector *= require ./sessionList *= require ./searchResults *= require ./clientUpdate diff --git a/web/app/views/clients/_musician_search_filter.html.slim b/web/app/views/clients/_musician_search_filter.html.slim index b02a4da41..e0b28b332 100644 --- a/web/app/views/clients/_musician_search_filter.html.slim +++ b/web/app/views/clients/_musician_search_filter.html.slim @@ -1,8 +1,13 @@ .content-body-scroller div#musician-search-filter-builder.content-wrapper h2 search musicians - a#btn-perform-musician-search.button-grey href="#" SEARCH + div#musician-search-filter-header + a#btn-perform-musician-search.button-grey href="#" SEARCH div#musician-search-filter-builder-form + div#musician-search-filter-builder-results + + div#musician-search-filter-results.content-wrapper + h2 search musicians script#template-musician-search-filter type="text/template" .field @@ -61,3 +66,54 @@ script#template-search-filter-setup-genres type="text/template" script#template-search-filter-setup-ages type="text/template" tr td {description} + +/! Session Row Template +script#template-search-musician-row type="text/template" + .profile-band-list-result.musician-list-result data-musician-id="{musician_id}" + .f11 data-hint="container" + .left.musician-avatar + /! avatar + .avatar-small + img src="{avatar_url}" / + .left.musician-info + .first-row data-hint="top-row" + .musician-profile + .result-name musician_name + .result-location musician_location + #result_instruments.instruments.nowrap.mt10 instruments + .musician-stats + span.friend-count + | {friend_count} + img align="absmiddle" alt="friends" height="12" src="../assets/content/icon_friend.png" style="margin-right:4px;" width="14" / + span.follower-count + | {follow_count} + img align="absmiddle" alt="followers" height="12" src="../assets/content/icon_followers.png" style="margin-right:4px;" width="22" / + span.recording-count + | {recording_count} + img align="absmiddle" alt="recordings" height="13" src="../assets/content/icon_recordings.png" style="margin-right:4px;" width="12" / + span.session-count + | {session_count} + img align="absmiddle" alt="sessions" height="12" src="../assets/content/icon_session_tiny.png" style="margin-right:4px;" width="12" / + br clear="both" / + .left.musician-latency + .latency-help + | Your latency + br>/ + | to {musician_first_name} is: + .latency-holder + | {latency_badge} + br clear="both" / + .button-row data-hint="button-row" + .biography biography + .result-list-button-wrapper data-musician-id="{musician_id}" + | {musician_action_template} + br clear="both" / + + +script#template-search-musician-action-btns type="text/template" + a.button-orange.smallbutton href="{profile_url}" PROFILE + - if current_user && current_user.musician? + a.smallbutton.search-m-friend class="{friend_class}" href="#" {friend_caption} + a.smallbutton.search-m-follow class="{follow_class}" href="#" {follow_caption} + a.smallbutton.search-m-message class="{message_class}" href="#" {message_caption" + .clearall From 4f7c43b9d87d286c184685f0b5c939d8c766583b Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 2 Apr 2015 14:52:16 +0000 Subject: [PATCH 089/195] VRFS-2795 cloning old search code into new musician_search --- ruby/lib/jam_ruby/models/musician_search.rb | 94 ++++++++++++++++++- .../musician_search_filter.js.coffee | 14 ++- web/app/controllers/api_search_controller.rb | 4 +- web/app/views/api_search/index.rabl | 51 ++++++++++ .../clients/_musician_search_filter.html.slim | 3 +- 5 files changed, 155 insertions(+), 11 deletions(-) diff --git a/ruby/lib/jam_ruby/models/musician_search.rb b/ruby/lib/jam_ruby/models/musician_search.rb index a7aa1e793..de28c053c 100644 --- a/ruby/lib/jam_ruby/models/musician_search.rb +++ b/ruby/lib/jam_ruby/models/musician_search.rb @@ -1,6 +1,8 @@ module JamRuby class MusicianSearch < JsonStore + attr_accessor :page_count, :results, :user_counters + ANY_VAL_STR = 'any' ANY_VAL_INT = -1 @@ -232,12 +234,100 @@ module JamRuby rel end - def search_results_page(filter={}, page=1) + def search_results_page(filter={}, page=1, searcher=nil) self.data_blob = filter self.save rel = do_search(filter) rel, page = self.pagination(rel, filter) - rel.to_json + rel = rel.includes([:instruments, :followings, :friends]) + @page_count = rel.total_pages + musician_results_for_user(rel.all, searcher) + end + + RESULT_FOLLOW = :follows + RESULT_FRIEND = :friends + + COUNT_FRIEND = :count_friend + COUNT_FOLLOW = :count_follow + COUNT_RECORD = :count_record + COUNT_SESSION = :count_session + COUNTERS = [COUNT_FRIEND, COUNT_FOLLOW, COUNT_RECORD, COUNT_SESSION] + + def musician_results_for_user(_results, user) + @results = _results + @user_counters = {} and return self unless user + + @user_counters = @results.inject({}) { |hh,val| hh[val.id] = []; hh } + mids = "'#{@results.map(&:id).join("','")}'" + + # this gets counts for each search result on friends/follows/records/sessions + @results.each do |uu| + counters = { } + counters[COUNT_FRIEND] = Friendship.where(:user_id => uu.id).count + counters[COUNT_FOLLOW] = Follow.where(:followable_id => uu.id).count + counters[COUNT_RECORD] = ClaimedRecording.where(:user_id => uu.id).count + counters[COUNT_SESSION] = MusicSession.where(:user_id => uu.id).count + @user_counters[uu.id] << counters + end + + # this section determines follow/like/friend status for each search result + # so that action links can be activated or not + rel = User.select("users.id AS uid") + rel = rel.joins("LEFT JOIN follows ON follows.user_id = '#{user.id}'") + rel = rel.where(["users.id IN (#{mids}) AND follows.followable_id = users.id"]) + rel.all.each { |val| @user_counters[val.uid] << RESULT_FOLLOW } + + rel = User.select("users.id AS uid") + rel = rel.joins("LEFT JOIN friendships AS friends ON friends.friend_id = '#{user.id}'") + rel = rel.where(["users.id IN (#{mids}) AND friends.user_id = users.id"]) + rel.all.each { |val| @user_counters[val.uid] << RESULT_FRIEND } + + self + end + + private + + def _count(musician, key) + if mm = @user_counters[musician.id] + return mm.detect { |ii| ii.is_a?(Hash) }[key] + end if @user_counters + 0 + end + + public + + def follow_count(musician) + _count(musician, COUNT_FOLLOW) + end + + def friend_count(musician) + _count(musician, COUNT_FRIEND) + end + + def record_count(musician) + _count(musician, COUNT_RECORD) + end + + def session_count(musician) + _count(musician, COUNT_SESSION) + end + + def is_friend?(musician) + if mm = @user_counters[musician.id] + return mm.include?(RESULT_FRIEND) + end if @user_counters + false + end + + def is_follower?(musician) + if mm = @user_counters[musician.id] + return mm.include?(RESULT_FOLLOW) + end if @user_counters + false + end + + def search_type + self.class.to_s end end diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index 2d7e89fb1..42819c604 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -10,6 +10,7 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter @userId = null @searchFilter = null @profileUtils = context.JK.ProfileUtils + @searchResults = null init: (app) => @screenBindings = { 'beforeShow': this.beforeShow, 'afterShow': this.afterShow } @@ -26,6 +27,7 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter userId = data.id afterShow: () => + $('#musician-search-filter-results').hide() this.renderSearchFilter() loadSearchFilter: (sFilter) => @@ -175,9 +177,10 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter didSearch: (response) => console.log("response = ", JSON.stringify(response)) - $('#musician-search-filter-builder-form').hide() - $('#musician-search-filter-builder-results').show() - this.renderMusicians(response) + $('#musician-search-filter-builder').hide() + $('#musician-search-filter-results').show() + @searchResults = response + this.renderMusicians performSearch: () => $.each gon.musician_search_meta.filter_keys.single, (index, key) => @@ -188,7 +191,7 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter @rest.postMusicianSearchFilter({ filter: JSON.stringify(@searchFilter), page: 1 }).done(this.didSearch) - renderMusicians: (musicianList) => + renderMusicians: () => ii = undefined len = undefined mTemplate = $('#template-search-musician-row').html() @@ -201,8 +204,9 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter follows = undefined followVals = undefined aFollow = undefined - myAudioLatency = musicianList.my_audio_latency + myAudioLatency = @searchResults.my_audio_latency ii = 0 + musicians = @searchResults.musicians len = musicians.length while ii < len musician = musicians[ii] diff --git a/web/app/controllers/api_search_controller.rb b/web/app/controllers/api_search_controller.rb index 69fe64db6..fa8a6f276 100644 --- a/web/app/controllers/api_search_controller.rb +++ b/web/app/controllers/api_search_controller.rb @@ -30,8 +30,8 @@ class ApiSearchController < ApiController logger.debug("*** params = #{params.inspect}") ms = MusicianSearch.user_search_filter(current_user) json = JSON.parse(params[:filter], :create_additions => false) - results = ms.search_results_page(json, [params[:page].to_i, 1].max) - render :json => results, :status => 200 + @search = ms.search_results_page(json, [params[:page].to_i, 1].max, current_user) + respond_with @search, responder: ApiResponder, status: 201, template: 'api_search/index' end end diff --git a/web/app/views/api_search/index.rabl b/web/app/views/api_search/index.rabl index 0f3479db6..e30c5d225 100644 --- a/web/app/views/api_search/index.rabl +++ b/web/app/views/api_search/index.rabl @@ -2,6 +2,56 @@ object @search node :search_type do |ss| ss.search_type end +if @search.is_a?(MusicianSearch) + + node :page_count do |foo| + @search.page_count + end + + node :my_audio_latency do |user| + current_user.last_jam_audio_latency.round if current_user.last_jam_audio_latency + end + + child(:results => :musicians) { + attributes :id, :first_name, :last_name, :name, :city, :state, :country, :online, :musician, :photo_url, :biography, :regionname, :score, :full_score + + node :is_friend do |musician| + @search.is_friend?(musician) + end + + node :is_following do |musician| + @search.is_follower?(musician) + end + + node :pending_friend_request do |musician| + musician.pending_friend_request?(current_user) + end + + node :biography do |musician| + musician.biography.nil? ? "" : musician.biography + end + + child :musician_instruments => :instruments do + attributes :instrument_id, :description, :proficiency_level, :priority + end + + child :top_followings => :followings do |uf| + node :user_id do |uu| uu.id end + node :photo_url do |uu| uu.photo_url end + node :name do |uu| uu.name end + end + + node :follow_count do |musician| @search.follow_count(musician) end + node :friend_count do |musician| @search.friend_count(musician) end + node :recording_count do |musician| @search.record_count(musician) end + node :session_count do |musician| @search.session_count(musician) end + + node :audio_latency do |musician| + last_jam_audio_latency(musician) + end + } +else + if @search.session_invite_search? child(:results => :suggestions) { node :value do |uu| uu.name end @@ -131,3 +181,4 @@ if @search.fans_text_search? } end +end diff --git a/web/app/views/clients/_musician_search_filter.html.slim b/web/app/views/clients/_musician_search_filter.html.slim index e0b28b332..29ce68637 100644 --- a/web/app/views/clients/_musician_search_filter.html.slim +++ b/web/app/views/clients/_musician_search_filter.html.slim @@ -4,10 +4,9 @@ div#musician-search-filter-header a#btn-perform-musician-search.button-grey href="#" SEARCH div#musician-search-filter-builder-form - div#musician-search-filter-builder-results div#musician-search-filter-results.content-wrapper - h2 search musicians + h2 musician search results script#template-musician-search-filter type="text/template" .field From 325d3d2780ebd841387f4e6d073682653d7d7605 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Thu, 2 Apr 2015 16:00:34 +0000 Subject: [PATCH 090/195] VRFS-2975 cloning findMusician js --- .../musician_search_filter.js.coffee | 92 +++++++++++++++---- 1 file changed, 76 insertions(+), 16 deletions(-) diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index 42819c604..764f65a8e 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -10,11 +10,15 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter @userId = null @searchFilter = null @profileUtils = context.JK.ProfileUtils + @helpBubble = context.JK.HelpBubbleHelper @searchResults = null + @instrument_logo_map = context.JK.getInstrumentIconMap24() init: (app) => @screenBindings = { 'beforeShow': this.beforeShow, 'afterShow': this.afterShow } app.bindScreen('musicians', @screenBindings); + @results = $('#musician-search-filter-results') + srchbtn = $ '#btn-perform-musician-search' srchbtn.on 'click', => this.performSearch() @@ -175,12 +179,22 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter vals.push $(this).val() vals + _formatLocation = (musician) -> + if musician.city and musician.state + musician.city + ', ' + musician.regionname + else if musician.city + musician.city + else if musician.state + musician.regionname + else + 'Location Unavailable' + didSearch: (response) => console.log("response = ", JSON.stringify(response)) $('#musician-search-filter-builder').hide() $('#musician-search-filter-results').show() @searchResults = response - this.renderMusicians + this.renderMusicians() performSearch: () => $.each gon.musician_search_meta.filter_keys.single, (index, key) => @@ -218,8 +232,8 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter ilen = musician['instruments'].length while jj < ilen toolTip = '' - if musician['instruments'][jj].instrument_id in instrument_logo_map - instr = instrument_logo_map[musician['instruments'][jj].instrument_id].asset + if musician['instruments'][jj].instrument_id in @instrument_logo_map + instr = @instrument_logo_map[musician['instruments'][jj].instrument_id].asset toolTip = musician['instruments'][jj].instrument_id instr_logos += '' jj++ @@ -233,12 +247,12 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter message_caption: 'MESSAGE' button_message: 'button-orange' musician_actions = context.JK.fillTemplate(aTemplate, actionVals) - latencyBadge = context._.template($templateAccountSessionLatency.html(), $.extend(sessionUtils.createLatency(musician), musician), variable: 'data') + latencyBadge = context._.template($("#template-account-session-latency").html(), $.extend(sessionUtils.createLatency(musician), musician), variable: 'data') mVals = avatar_url: context.JK.resolveAvatarUrl(musician.photo_url) profile_url: '/client#/profile/' + musician.id musician_name: musician.name - musician_location: formatLocation(musician) + musician_location: _formatLocation(musician) instruments: instr_logos biography: musician['biography'] follow_count: musician['follow_count'] @@ -250,7 +264,7 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter latency_badge: latencyBadge musician_first_name: musician['first_name'] $rendering = $(context.JK.fillTemplate(mTemplate, mVals)) - $offsetParent = $results.closest('.content') + $offsetParent = @results.closest('.content') data = entity_type: 'musician' options = positions: [ @@ -261,16 +275,62 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter ] offsetParent: $offsetParent scoreOptions = offsetParent: $offsetParent - context.JK.helpBubble $('.follower-count', $rendering), 'follower-count', data, options - context.JK.helpBubble $('.friend-count', $rendering), 'friend-count', data, options - context.JK.helpBubble $('.recording-count', $rendering), 'recording-count', data, options - context.JK.helpBubble $('.session-count', $rendering), 'session-count', data, options - helpBubble.scoreBreakdown $('.latency', $rendering), false, musician['full_score'], myAudioLatency, musician['audio_latency'], musician['score'], scoreOptions - $results.append $rendering + context.JK.helpBubble($('.follower-count', $rendering), 'follower-count', data, options); + context.JK.helpBubble($('.friend-count', $rendering), 'friend-count', data, options); + context.JK.helpBubble($('.recording-count', $rendering), 'recording-count', data, options); + context.JK.helpBubble($('.session-count', $rendering), 'session-count', data, options); + @helpBubble.scoreBreakdown $('.latency', $rendering), false, musician['full_score'], myAudioLatency, musician['audio_latency'], musician['score'], scoreOptions + @results.append $rendering $rendering.find('.biography').dotdotdot() ii++ - $('.search-m-friend').on 'click', friendMusician - $('.search-m-follow').on 'click', followMusician - $('.search-m-message').on 'click', messageMusician + $('.search-m-friend').on 'click', this.friendMusician + $('.search-m-follow').on 'click', this.followMusician + $('.search-m-message').on 'click', this.messageMusician context.JK.bindHoverEvents() - return \ No newline at end of file + return + + friendMusician: (evt) => + # if the musician is already a friend, remove the button-orange class, and prevent + # the link from working + if 0 == $(this).closest('.button-orange').size() + return false + $(this).click (ee) -> + ee.preventDefault() + return + evt.stopPropagation() + uid = $(this).parent().data('musician-id') + rest.sendFriendRequest app, uid, friendRequestCallback + return + + followMusician: (evt) => + # if the musician is already followed, remove the button-orange class, and prevent + # the link from working + if 0 == $(this).closest('.button-orange').size() + return false + $(this).click (ee) -> + ee.preventDefault() + return + evt.stopPropagation() + newFollowing = {} + newFollowing.user_id = $(this).parent().data('musician-id') + url = '/api/users/' + context.JK.currentUserId + '/followings' + $.ajax + type: 'POST' + dataType: 'json' + contentType: 'application/json' + url: url + data: JSON.stringify(newFollowing) + processData: false + success: (response) -> + # remove the orange look to indicate it's not selectable + # @FIXME -- this will need to be tweaked when we allow unfollowing + $('div[data-musician-id=' + newFollowing.user_id + '] .search-m-follow').removeClass('button-orange').addClass 'button-grey' + return + error: app.ajaxError + return + + messageMusician: () => + userId = $(this).parent().data('musician-id') + app.layout.showDialog 'text-message', d1: userId + false + \ No newline at end of file From 1f432446f59900341bbd1d6f47e3a3ea24b0fb55 Mon Sep 17 00:00:00 2001 From: Jonathan Kolyer Date: Sat, 4 Apr 2015 02:54:58 +0000 Subject: [PATCH 091/195] VRFS-2795 fixing results display; generating same data --- .../musician_search_filter.js.coffee | 17 +++--- .../clients/_musician_search_filter.html.slim | 14 +++-- web/lib/tasks/sample_data.rake | 55 +++++++++++++++---- 3 files changed, 60 insertions(+), 26 deletions(-) diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index 764f65a8e..4cd0d4b21 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -32,6 +32,7 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter afterShow: () => $('#musician-search-filter-results').hide() + $('#musician-search-filter-builder').show() this.renderSearchFilter() loadSearchFilter: (sFilter) => @@ -181,16 +182,16 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter _formatLocation = (musician) -> if musician.city and musician.state - musician.city + ', ' + musician.regionname + musician.city + ', ' + musician.state else if musician.city musician.city - else if musician.state + else if musician.regionname musician.regionname else 'Location Unavailable' didSearch: (response) => - console.log("response = ", JSON.stringify(response)) + # console.log("response = ", JSON.stringify(response)) $('#musician-search-filter-builder').hide() $('#musician-search-filter-results').show() @searchResults = response @@ -207,14 +208,12 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter renderMusicians: () => ii = undefined - len = undefined mTemplate = $('#template-search-musician-row').html() aTemplate = $('#template-search-musician-action-btns').html() mVals = undefined musician = undefined renderings = '' instr_logos = undefined - instr = undefined follows = undefined followVals = undefined aFollow = undefined @@ -231,11 +230,9 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter jj = 0 ilen = musician['instruments'].length while jj < ilen - toolTip = '' - if musician['instruments'][jj].instrument_id in @instrument_logo_map - instr = @instrument_logo_map[musician['instruments'][jj].instrument_id].asset - toolTip = musician['instruments'][jj].instrument_id - instr_logos += '' + instr_id = musician['instruments'][jj].instrument_id + if instr_img = @instrument_logo_map[instr_id] + instr_logos += '' jj++ actionVals = profile_url: '/client#/profile/' + musician.id diff --git a/web/app/views/clients/_musician_search_filter.html.slim b/web/app/views/clients/_musician_search_filter.html.slim index 29ce68637..2e6d5ecd4 100644 --- a/web/app/views/clients/_musician_search_filter.html.slim +++ b/web/app/views/clients/_musician_search_filter.html.slim @@ -77,9 +77,12 @@ script#template-search-musician-row type="text/template" .left.musician-info .first-row data-hint="top-row" .musician-profile - .result-name musician_name - .result-location musician_location - #result_instruments.instruments.nowrap.mt10 instruments + .result-name + | {musician_name} + .result-location + | {musician_location} + #result_instruments.instruments.nowrap.mt10 + | {instruments} .musician-stats span.friend-count | {friend_count} @@ -97,13 +100,14 @@ script#template-search-musician-row type="text/template" .left.musician-latency .latency-help | Your latency - br>/ + br / | to {musician_first_name} is: .latency-holder | {latency_badge} br clear="both" / .button-row data-hint="button-row" - .biography biography + .biography + | {biography} .result-list-button-wrapper data-musician-id="{musician_id}" | {musician_action_template} br clear="both" / diff --git a/web/lib/tasks/sample_data.rake b/web/lib/tasks/sample_data.rake index c19d46dea..3dbf3bbf5 100644 --- a/web/lib/tasks/sample_data.rake +++ b/web/lib/tasks/sample_data.rake @@ -34,10 +34,13 @@ namespace :db do end task populate: :environment do - make_users(10) if 14 > User.count + make_users(30) if 14 > User.count make_friends + make_followings make_bands make_band_members + # make_music_sessions_history + # make_music_sessions_user_history make_recording end @@ -90,16 +93,18 @@ end def make_music_sessions_history users = User.all.map(&:id) bands = Band.all.map(&:id) - genres = Genre.all.map(&:description) - 50.times do |nn| + genres = Genre.all + 20.times do |nn| obj = MusicSession.new obj.music_session_id = rand(100000000).to_s - obj.description = Faker::Lorem.paragraph + obj.description = 'description goes here' # Faker::Lorem.paragraph avoid accidental profanity obj.user_id = users[rand(users.count)] obj.band_id = bands[rand(bands.count)] obj.created_at = Time.now - rand(1.month.seconds) obj.session_removed_at = obj.created_at + (rand(3)+1).hour - obj.genres = genres.shuffle[0..rand(4)].join(' | ') + obj.genre = genres.sample + obj.legal_terms = true + obj.name = Faker::Lorem.sentence obj.save! end end @@ -171,20 +176,45 @@ end def make_users(num=99) admin = User.create!(first_name: Faker::Name.name, last_name: Faker::Name.name, - email: "example@railstutorial.org", + email: Faker::Internet.safe_email, password: "foobar", password_confirmation: "foobar", terms_of_service: true) admin.toggle!(:admin) + + instruments = Instrument.all num.times do |n| - email = "example-#{n+1}@railstutorial.org" password = "password" - User.create!(first_name: Faker::Name.name, + uu = User.create!(first_name: Faker::Name.name, last_name: Faker::Name.name, terms_of_service: true, - email: email, + email: Faker::Internet.email, password: password, + city: Faker::Address.city, + state: Faker::Address.state_abbr, + country: 'US', password_confirmation: password) + uu.musician = true + num_instrument = rand(4) + 1 + user_instruments = instruments.sample(num_instrument) + num_instrument.times do |mm| + musician_instrument = MusicianInstrument.new + musician_instrument.user = uu + musician_instrument.instrument = user_instruments[mm] + musician_instrument.proficiency_level = rand(3) + 1 + musician_instrument.priority = rand(num_instrument) + uu.musician_instruments << musician_instrument + end + uu.save! + yn = true + while yn + begin + uu.biography = Faker::Lorem.sentence + uu.save! + yn = false + rescue + end + end end end @@ -209,8 +239,11 @@ def make_followings users = User.all users.each do |uu| users[0..rand(users.count)].shuffle.each do |uuu| - uuu.followings << uu unless 0 < Follow.where(:followable_id => uu.id, :user_id => uuu.id).count - uu.followings << uuu unless 0 < Follow.where(:followable_id => uuu.id, :user_id => uu.id).count if rand(3)==0 + next if 0 < Follow.where(:followable_id => uu.id, :user_id => uuu.id).count + follow = Follow.new + follow.followable = uu + follow.user = uuu + follow.save end end end From 2f8d048ddedddcdeefb7cb0711752145405bc3d7 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 4 Apr 2015 02:31:12 -0400 Subject: [PATCH 092/195] VRFS-2701 updated site validator lib to extract recording title for YouTube and SoundCloud URLs --- .../javascripts/site_validator.js.coffee | 5 +++- web/app/controllers/api_users_controller.rb | 7 +++--- .../clients/_account_profile_samples.html.erb | 25 ++++++++++++++++--- web/lib/utils.rb | 23 ++++++++++++++--- 4 files changed, 49 insertions(+), 11 deletions(-) diff --git a/web/app/assets/javascripts/site_validator.js.coffee b/web/app/assets/javascripts/site_validator.js.coffee index 0c450cd72..0ce2c645a 100644 --- a/web/app/assets/javascripts/site_validator.js.coffee +++ b/web/app/assets/javascripts/site_validator.js.coffee @@ -149,9 +149,12 @@ context.JK.RecordingSourceValidator = class RecordingSourceValidator extends Sit @add_btn.removeClass('disabled') if @site_status - @recording_sources.push({ url: response.data, recording_id: response.recording_id }) + @recording_sources.push({ url: response.data, recording_id: response.recording_id, recording_title: response.recording_title }) if @site_success_callback @site_success_callback(@input_div) + else + if @site_fail_callback + @site_fail_callback(@input_div) processSiteCheckFail: (response) => super(response) diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index a6ebb81c3..947976474 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -760,9 +760,10 @@ class ApiUsersController < ApiController if site.blank? || 'url'==site url = data elsif Utils.recording_source?(site) - rec_id = Utils.extract_recording_id(site, data) - if rec_id - render json: { message: 'Valid Site', recording_id: rec_id, data: data }, status: 200 + rec_data = Utils.extract_recording_data(site, data) + binding.pry + if rec_data + render json: { message: 'Valid Site', recording_id: rec_data["id"], recording_title: rec_data["title"], data: data }, status: 200 return else render json: { message: 'Invalid Site', data: data, errors: { site: ["Could not detect recording identifier"] } }, status: 200 diff --git a/web/app/views/clients/_account_profile_samples.html.erb b/web/app/views/clients/_account_profile_samples.html.erb index e295830e0..c4141b341 100644 --- a/web/app/views/clients/_account_profile_samples.html.erb +++ b/web/app/views/clients/_account_profile_samples.html.erb @@ -172,10 +172,10 @@ window.twitterValidator = new JK.SiteValidator('twitter', userNameSuccessCallback, userNameFailCallback); twitterValidator.init(); - window.soundCloudRecordingValidator = new JK.RecordingSourceValidator('rec_soundcloud', siteSuccessCallback, siteFailCallback); + window.soundCloudRecordingValidator = new JK.RecordingSourceValidator('rec_soundcloud', soundCloudSuccessCallback, siteFailCallback); soundCloudRecordingValidator.init(); - window.youTubeRecordingValidator = new JK.RecordingSourceValidator('rec_youtube', siteSuccessCallback, siteFailCallback); + window.youTubeRecordingValidator = new JK.RecordingSourceValidator('rec_youtube', youTubeSuccessCallback, siteFailCallback); youTubeRecordingValidator.init(); }, 1); @@ -190,7 +190,7 @@ $inputDiv.append("Invalid username").show(); } - function siteSuccessCallback($inputDiv) { + function soundCloudSuccessCallback($inputDiv) { $inputDiv.removeClass('error'); $inputDiv.find('.error-text').remove(); @@ -200,7 +200,24 @@ var $sampleList = $soundCloudSampleList.find('.sample-list'); var addedRecording = recordingSources[recordingSources.length-1]; $sampleList.append('
'); - $sampleList.append(addedRecording.url); + $sampleList.append(addedRecording.recording_title); + $sampleList.append('
'); + } + + $inputDiv.find('input').val(''); + } + + function youTubeSuccessCallback($inputDiv) { + $inputDiv.removeClass('error'); + $inputDiv.find('.error-text').remove(); + + var recordingSources = window.youTubeRecordingValidator.recordingSources(); + if (recordingSources && recordingSources.length > 0) { + console.log('recordingSources=%o', recordingSources); + var $sampleList = $youTubeSampleList.find('.sample-list'); + var addedRecording = recordingSources[recordingSources.length-1]; + $sampleList.append('
'); + $sampleList.append(addedRecording.recording_title); $sampleList.append('
'); } diff --git a/web/lib/utils.rb b/web/lib/utils.rb index 80fe08cbe..0659b3dcf 100644 --- a/web/lib/utils.rb +++ b/web/lib/utils.rb @@ -12,14 +12,26 @@ class Utils RECORDING_SOURCES.include?(site) end - def self.extract_recording_id(site, recording_url) + def self.extract_recording_data(site, recording_url) recording_url.strip! + rec_data = {} case site when 'rec_youtube' # regex derived from: https://gist.github.com/afeld/1254889 if recording_url =~ /(youtu.be\/|youtube.com\/(watch\?(.*&)?v=|(embed|v)\/))([^\?&\"\'>]+)/ - return $5 + rec_data["id"] = $5 end + + uri = URI.parse("https://gdata.youtube.com/feeds/api/videos/#{$5}?v=2&alt=json") + + http = Net::HTTP.new(uri.host, uri.port) + http.use_ssl = true if uri.scheme == 'https' + req = Net::HTTP::Get.new(uri) + response = http.request(req) + json = JSON.parse(response.body) if response + rec_data["title"] = json["entry"]["title"]["$t"] + return rec_data unless rec_data.empty? + when 'rec_soundcloud' if recording_url =~ /^https?:\/\/.*soundcloud.com\/.+/ tmpfile = Tempfile.new(site) @@ -29,8 +41,13 @@ class Utils result = File.read(tmpfile.path) File.delete(tmpfile.path) if result =~ /"soundcloud:\/\/sounds:(\d+)"/ - return $1 + rec_data["id"] = $1 end + + if result =~ /property=\"og:title\" content=\"([\w\W]+)\"> Date: Sat, 4 Apr 2015 02:32:00 -0400 Subject: [PATCH 093/195] remove binding.pry --- web/app/controllers/api_users_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index 947976474..30a8a7ebf 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -761,7 +761,6 @@ class ApiUsersController < ApiController url = data elsif Utils.recording_source?(site) rec_data = Utils.extract_recording_data(site, data) - binding.pry if rec_data render json: { message: 'Valid Site', recording_id: rec_data["id"], recording_title: rec_data["title"], data: data }, status: 200 return From 5bd5b264067e18f9942d48489a45c55fa3326b5e Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 4 Apr 2015 09:59:54 -0400 Subject: [PATCH 094/195] VRFS-2701 consolidated site add code / added X to remove row --- .../client/accountProfileSamples.css.scss | 13 ++++++- .../clients/_account_profile_samples.html.erb | 39 ++++++++----------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss index 11b3bb704..f32f9ce14 100644 --- a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss +++ b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss @@ -33,9 +33,20 @@ div.sample-list { height: 250px; - width: 250px; + width: 325px; border: 2px solid #ccc; overflow: auto; } + + div.entry { + margin-left: 10px; + margin-top: 5px; + } + + div.close-button { + margin-right: 15px; + margin-top: 5px; + cursor: pointer; + } } } \ No newline at end of file diff --git a/web/app/views/clients/_account_profile_samples.html.erb b/web/app/views/clients/_account_profile_samples.html.erb index c4141b341..7fd19cd5d 100644 --- a/web/app/views/clients/_account_profile_samples.html.erb +++ b/web/app/views/clients/_account_profile_samples.html.erb @@ -95,7 +95,6 @@
-  
@@ -108,7 +107,6 @@
-  
@@ -191,37 +189,34 @@ } function soundCloudSuccessCallback($inputDiv) { + siteSuccessCallback($inputDiv, window.soundCloudRecordingValidator, $soundCloudSampleList); + } + + function youTubeSuccessCallback($inputDiv) { + siteSuccessCallback($inputDiv, window.youTubeRecordingValidator, $youTubeSampleList); + } + + function siteSuccessCallback($inputDiv, recordingSiteValidator, $sampleList) { $inputDiv.removeClass('error'); $inputDiv.find('.error-text').remove(); - var recordingSources = window.soundCloudRecordingValidator.recordingSources(); + var recordingSources = recordingSiteValidator.recordingSources(); if (recordingSources && recordingSources.length > 0) { console.log('recordingSources=%o', recordingSources); - var $sampleList = $soundCloudSampleList.find('.sample-list'); + var $sampleList = $sampleList.find('.sample-list'); var addedRecording = recordingSources[recordingSources.length-1]; - $sampleList.append('
'); - $sampleList.append(addedRecording.recording_title); - $sampleList.append('
'); + var recordingIdAttr = ' data-recording-id="' + addedRecording.recording_id + '" '; + var recordingUrlAttr = ' data-recording-url="' + addedRecording.url + '"'; + var title = formatTitle(addedRecording.recording_title); + $sampleList.append('
' + title + '
'); + $sampleList.append('
X
'); } $inputDiv.find('input').val(''); } - function youTubeSuccessCallback($inputDiv) { - $inputDiv.removeClass('error'); - $inputDiv.find('.error-text').remove(); - - var recordingSources = window.youTubeRecordingValidator.recordingSources(); - if (recordingSources && recordingSources.length > 0) { - console.log('recordingSources=%o', recordingSources); - var $sampleList = $youTubeSampleList.find('.sample-list'); - var addedRecording = recordingSources[recordingSources.length-1]; - $sampleList.append('
'); - $sampleList.append(addedRecording.recording_title); - $sampleList.append('
'); - } - - $inputDiv.find('input').val(''); + function formatTitle(title) { + return title && title.length > 30 ? title.substring(0, 30) : title; } function siteFailCallback($inputDiv) { From 5ae708a898cd14ffe222e2bc879fcdd1a36fc397 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Sat, 4 Apr 2015 10:51:48 -0400 Subject: [PATCH 095/195] VRFS-2701 added ability to remove recordings from list --- .../clients/_account_profile_samples.html.erb | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/web/app/views/clients/_account_profile_samples.html.erb b/web/app/views/clients/_account_profile_samples.html.erb index 7fd19cd5d..6af344f03 100644 --- a/web/app/views/clients/_account_profile_samples.html.erb +++ b/web/app/views/clients/_account_profile_samples.html.erb @@ -131,6 +131,18 @@
- - diff --git a/web/app/views/clients/_band_setup.html.noterb b/web/app/views/clients/_band_setup.html.noterb deleted file mode 100644 index fc31a7170..000000000 --- a/web/app/views/clients/_band_setup.html.noterb +++ /dev/null @@ -1,155 +0,0 @@ - -
-
-
- <%= image_tag "content/icon_bands.png", :size => "19x19" %> -
- -

set up band

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

Step 1: General Information

-
- - <%= image_tag "shared/avatar_generic_band.png", {:id => "band-avatar", :align=>"absmiddle", :height => 88, :width => 88 } %> - -

- Upload band photo.

- -
-
- -
-
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- - -
-
-
- -
-
-
-
-
-
- - -
-
-
-
- CANCEL   - NEXT -
-
-
- -
-
-
-
-
- - - - diff --git a/web/app/views/clients/_band_setup.html.slim b/web/app/views/clients/_band_setup.html.slim index 0e8a62bb2..c19db17e0 100644 --- a/web/app/views/clients/_band_setup.html.slim +++ b/web/app/views/clients/_band_setup.html.slim @@ -1,4 +1,4 @@ -#band-setup.screen.secondary[layout="screen" layout-id="band/setup" layout-arg="id"] +#band-setup.screen.secondary layout="screen" layout-id="band/setup" layout-arg="id" .content-head .content-icon = image_tag "content/icon_bands.png", :size => "19x19" @@ -8,77 +8,75 @@ .content-body .content-body-scroller form#band-setup-form - div[style="display:block;"] - #band-setup-step-1.content-wrapper[style="padding:10px 35px 10px 35px;"] + div style="display:block;" + #band-setup-step-1.content-wrapper style="padding:10px 35px10px 35px;" br - table[width="100%" cellpadding="0" cellspacing="0"] + table width="100%" cellpadding="0" cellspacing="0" tr - td[valign="top" colspan="2"] + td colspan="2" h2 span.band-setup-text-step1 - | Step 1: - | General Information - td#tdBandPhoto[valign="middle" rowspan="2" width="33%"] - a.avatar-profile[href="#"] - = image_tag "shared/avatar_generic_band.png", {:id => "band-avatar", :align=>"absmiddle", :height => 88, :width => 88 } - br - br - a#band-change-photo.small.ml20[href="#"] - | Upload band photo. - br[clear="all"] - br + | Step 1: General Information + tr#name_row + td colspan="2" + .band-photo.hidden + span.field + a.band-avatar-profile href="#" + = image_tag "shared/avatar_generic_band.png", {:id => "band-avatar", :align=>"absmiddle", :height => 88, :width => 88 } + span.field + a#band-change-photo.small.ml20 href="#" + | Upload band photo. + + .band-name + .field + label for="band-name" + | Band Name: + input#band-name type="text" maxlength="1024" value="" + / td#tdBandWebsite[] + / .field + / label for="band-website" + / | Web Site: + / input#band-website[type="text" maxlength="4000" value=""] tr - td#tdBandName[valign="middle" width="33%"] + td.band-country .field - label[for="band-name"] - | Band Name: - input#band-name.w80[type="text" maxlength="1024" value=""] - br - td#tdBandWebsite[valign="middle" width="33%"] - .field - label[for="band-website"] - | Web Site: - input#band-website.w80[type="text" maxlength="4000" value=""] - tr - td#tdBandCountry[valign="middle"] - .field - label[for="band-country"] + label for="band-country" | Country: - select#band-country.w80 - - td#tdBandRegion[valign="middle"] + select#band-country + td#tdBandBiography rowspan="3" .field - label[for="band-region"] - | State/Region: - select#band-region.w80 - - td#tdBandCity[valign="middle"] - .field - label[for="band-city"] - | City: - select#band-city.w80 - - tr - td#tdBandGenres[valign="top"] - .field - label[for="band-genres"] - | Genres: - .band-setup-genres.w90 - table#band-genres[width="100%" cellpadding="10" cellspacing="6"] - td#tdBandBiography[valign="top" colspan="2"] - .field - label[for="band-biography"] + label for="band-biography" | Description / Bio: - textarea#band-biography.band-setup-bio.w90 - br[clear="all"] + textarea#band-biography rows="8" + tr + td.band-region + .field + label for="band-region" + | State/Region: + select#band-region + tr + td.band-city + .field + label for="band-city" + | City: + select#band-city + + / td#tdBandGenres[valign="top"] + / .field + / label for="band-genres" + / | Genres: + / .band-setup-genres.w90 + / table#band-genres[width="100%" cellpadding="10" cellspacing="6"] + + br clear="all" .right a#btn-band-setup-cancel.button-grey | CANCEL |    a#btn-band-setup-next.button-orange - | NEXT + | SAVE & NEXT .clearall - #band-setup-step-2.content-wrapper[style="padding:10px 35px 10px 35px; display:none;"] + #band-setup-step-2.content-wrapper style="padding:10px 35px 10px 35px; display:none;" br h2 span.band-setup-text-step2 @@ -109,7 +107,7 @@ = image_tag("content/icon_google.png", :size => "24x24", :align => "absmiddle") .right.mt5.ml5 | Google+ - br[clear="all"] + br clear="all" .right a#btn-band-setup-back.button-grey | BACK @@ -117,11 +115,17 @@ a#btn-band-setup-save.button-orange | CREATE BAND .clearall -script#template-band-setup-genres[type="text/template"] +script#template-band-setup-genres type="text/template" tr - td {description + tr + td + input value="{id}" {checked} type="checkbox" + | {description} + +script#template-band-invitation type="text/template" + .invitation user-id="{userId}" + | {userName} + a + img src="shared/icon_delete_sm.png" width="13" height="13" + -script#template-band-invitation[type="text/template"] -.invitation user-id="{userId}" - | {userName} - a =image_tag "shared/icon_delete_sm.png", :size => "13x13" From f16bd28574cd09894ffd9fbd7ae51b73d284af5a Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Tue, 19 May 2015 13:32:15 -0500 Subject: [PATCH 149/195] VRFS-3243 : Ability to selectively skip genre validation --- ruby/lib/jam_ruby/models/band.rb | 7 ++++--- web/app/assets/javascripts/band_setup.js | 13 +++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/ruby/lib/jam_ruby/models/band.rb b/ruby/lib/jam_ruby/models/band.rb index e9fb548c0..a0c3a5033 100644 --- a/ruby/lib/jam_ruby/models/band.rb +++ b/ruby/lib/jam_ruby/models/band.rb @@ -9,7 +9,7 @@ module JamRuby :band_type, :band_status, :concert_count, :add_new_members, :play_commitment, :touring_option, :paid_gigs, :free_gigs, :hourly_rate, :gig_minimum - attr_accessor :updating_photo, :skip_location_validation + attr_accessor :updating_photo, :skip_location_validation, :skip_genre_validation self.primary_key = 'id' @@ -21,7 +21,7 @@ module JamRuby validates :city, presence: true, :unless => :skip_location_validation validate :validate_photo_info - validate :require_at_least_one_genre + validate :require_at_least_one_genre, :unless => :skip_genre_validation validate :limit_max_genres before_save :check_lat_lng @@ -199,7 +199,8 @@ module JamRuby band.genres = genres end - + band.skip_genre_validation = true unless params[:validate_genres] + puts "SKIPPING GENRE VALIDATION: #{band.skip_genre_validation}" band end diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 429612727..d6cc2cd31 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -22,6 +22,8 @@ var nilOptionText = 'n/a'; var bandId = ''; var friendInput=null; + + // TODO: Use a single variable for a mutually exclusive option: var step1, step2; var isSaving = false; @@ -123,7 +125,13 @@ band.city = $("#band-city").val(); band.state = $("#band-region").val(); band.country = $("#band-country").val(); - band.genres = getSelectedGenres(); + + if (step2) { + band.genres = getSelectedGenres(); + band.validate_genres = true + } else { + band.validate_genres = false + } return band; } @@ -240,7 +248,6 @@ $("#band-setup-title").html("edit band"); $("#btn-band-setup-save").html("SAVE CHANGES"); $("#band-change-photo").html('Upload band photo.'); - //$("#name_row").after($("#tdBandPhoto")); $('.band-photo').removeClass("hidden") // retrieve and initialize band profile data points @@ -472,6 +479,8 @@ $("#band-setup-step-1").hide(); } else if (step1) { saveBand(); + step1=false + step2=true } }) .fail(function (jqXHR) { From 69a8f1e8bedd4ca7c35717078871fb01273f390e Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Tue, 19 May 2015 13:33:07 -0500 Subject: [PATCH 150/195] VRFS-3243 : Some general style and code cleanup. --- web/app/assets/stylesheets/client/band.css.scss | 11 +++++++---- web/app/views/clients/_bandProfile.html.erb | 2 +- web/app/views/clients/_band_setup.html.slim | 8 +++----- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/web/app/assets/stylesheets/client/band.css.scss b/web/app/assets/stylesheets/client/band.css.scss index abc00ce4a..b2228bf5b 100644 --- a/web/app/assets/stylesheets/client/band.css.scss +++ b/web/app/assets/stylesheets/client/band.css.scss @@ -1,15 +1,13 @@ @import "client/common.css.scss"; -#band-setup { - padding: 0.25em; - +#band-setup, #band-profile { input, select, textarea { @include border_box_sizing; width: 100%; padding: 2px 4px 2px 2px; } - #tdBandBiography { + td.band-biography { height:100%; vertical-align: top; #band-biography { @@ -349,6 +347,11 @@ } #band-setup-form { + padding: 0.25em 0.5em 0.25em 0.25em; + #band-setup-table { + + } + .easydropdown { padding: 0 3px; width:100%; diff --git a/web/app/views/clients/_bandProfile.html.erb b/web/app/views/clients/_bandProfile.html.erb index 3f58378ee..1a51223bd 100644 --- a/web/app/views/clients/_bandProfile.html.erb +++ b/web/app/views/clients/_bandProfile.html.erb @@ -1,5 +1,5 @@ -
+
<%= image_tag "content/icon_bands.png", :size => "19x19" %> diff --git a/web/app/views/clients/_band_setup.html.slim b/web/app/views/clients/_band_setup.html.slim index c19db17e0..a9f98a923 100644 --- a/web/app/views/clients/_band_setup.html.slim +++ b/web/app/views/clients/_band_setup.html.slim @@ -11,12 +11,10 @@ div style="display:block;" #band-setup-step-1.content-wrapper style="padding:10px 35px10px 35px;" br - table width="100%" cellpadding="0" cellspacing="0" + table width="100%" tr td colspan="2" - h2 - span.band-setup-text-step1 - | Step 1: General Information + h2 | Step 1: General Information tr#name_row td colspan="2" .band-photo.hidden @@ -43,7 +41,7 @@ label for="band-country" | Country: select#band-country - td#tdBandBiography rowspan="3" + td.band-biography rowspan="3" .field label for="band-biography" | Description / Bio: From 1c93cdb7979865557e7b6377d4caf42c6ca119f9 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Tue, 19 May 2015 13:33:25 -0500 Subject: [PATCH 151/195] VRFS-3243 : Fix tests --- web/spec/features/bands_spec.rb | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/web/spec/features/bands_spec.rb b/web/spec/features/bands_spec.rb index 1d7281f44..3c74a4fda 100644 --- a/web/spec/features/bands_spec.rb +++ b/web/spec/features/bands_spec.rb @@ -34,7 +34,8 @@ describe "Bands", :js => true, :type => :feature, :capybara_feature => true do params.each do |field, value| fill_in field, with: "#{value}" end - first('#band-genres input[type="checkbox"]').trigger(:click) + # Move to experience pane: + #first('#band-genres input[type="checkbox"]').trigger(:click) end sleep 1 # work around race condition @@ -68,9 +69,9 @@ describe "Bands", :js => true, :type => :feature, :capybara_feature => true do it "indicates required fields and user may eventually complete" do navigate_band_setup find('#btn-band-setup-next').trigger(:click) - expect(page).to have_selector('#tdBandName .error-text li', text: "can't be blank") - expect(page).to have_selector('#tdBandBiography .error-text li', text: "can't be blank") - expect(page).to have_selector('#tdBandGenres .error-text li', text: "At least 1 genre is required.") + expect(page).to have_selector('#band-setup .band-name .error-text li', text: "can't be blank") + expect(page).to have_selector('#band-setup .band-biography .error-text li', text: "can't be blank") + #expect(page).to have_selector('#band-setup .band-genres .error-text li', text: "At least 1 genre is required.") complete_band_setup_form("Band name", "Band biography") @@ -79,6 +80,7 @@ describe "Bands", :js => true, :type => :feature, :capybara_feature => true do end it "limits genres to 3" do + pending "Move this to experience pane" genres = Genre.limit(4) navigate_band_setup within('#band-setup-form') do @@ -89,7 +91,7 @@ describe "Bands", :js => true, :type => :feature, :capybara_feature => true do end end find('#btn-band-setup-next').trigger(:click) - expect(page).to have_selector('#tdBandGenres .error-text li', text: "No more than 3 genres are allowed.") + expect(page).to have_selector('#band-setup .band-genres .error-text li', text: "No more than 3 genres are allowed.") end it "handles max-length field input" do @@ -106,11 +108,11 @@ describe "Bands", :js => true, :type => :feature, :capybara_feature => true do within('#band-setup-form') do fill_in 'band-name', with: band_name fill_in 'band-biography', with: band_bio - all('#band-genres input[type="checkbox"]').first.trigger(:click) + #all('#band-genres input[type="checkbox"]').first.trigger(:click) end sleep 1 find('#btn-band-setup-next').trigger(:click) - expect(page).to have_selector('#tdBandBiography .error-text li', text: "is too long (maximum is 4000 characters)") + #expect(page).to have_selector('#band-biography .error-text li', text: "is too long (maximum is 4000 characters)") end it "handles special characters in text fields" do @@ -118,7 +120,7 @@ describe "Bands", :js => true, :type => :feature, :capybara_feature => true do band_name = garbage(3) + ' ' + garbage(50) band_bio = garbage(500) band_website = garbage(2000) - complete_band_setup_form(band_name, band_bio, 'band-website' => band_website) + complete_band_setup_form(band_name, band_bio)#, 'band-website' => band_website) expect(page).to have_selector('#band-profile-name', text: Sanitize.fragment(band_name)) expect(page).to have_selector('#band-profile-biography', text: Sanitize.fragment(band_bio)) @@ -179,7 +181,7 @@ describe "Bands", :js => true, :type => :feature, :capybara_feature => true do band_bio = "Good, good friends" band_website = "http://www.sounds.com/thetwoguysfrom2009.html" - fill_out_band_setup_form(band_name, band_bio, 'band-website' => band_website) + fill_out_band_setup_form(band_name, band_bio)#, 'band-website' => band_website) #invite somebody using the picker find('#btn-choose-friends-band').trigger(:click) @@ -233,7 +235,7 @@ describe "Bands", :js => true, :type => :feature, :capybara_feature => true do expect(page).to have_selector('#btn-edit-band-profile') find('#btn-edit-band-profile').trigger(:click) - find('h2', text: 'General Information') + find('h2', text: 'Step 1: General Information') expect(page).to have_content band.name expect(page).to have_content band.biography From 429d5ad35dde40a50432ae99d37b3f86599b6d9c Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Wed, 20 May 2015 14:17:53 -0500 Subject: [PATCH 152/195] VRFS-3244 : New navigation logic for band profile editor -- incremental. --- web/app/assets/javascripts/bandProfile.js | 2 +- web/app/assets/javascripts/band_setup.js | 267 +++++++++++++------- web/app/assets/javascripts/profile.js | 4 +- web/app/views/clients/_band_setup.html.slim | 58 +++-- 4 files changed, 215 insertions(+), 116 deletions(-) diff --git a/web/app/assets/javascripts/bandProfile.js b/web/app/assets/javascripts/bandProfile.js index c140d2ea2..1570a8519 100644 --- a/web/app/assets/javascripts/bandProfile.js +++ b/web/app/assets/javascripts/bandProfile.js @@ -481,7 +481,7 @@ $('#band-profile-social-link').unbind('click').click(renderSocial); $("#btn-edit-band-profile").unbind('click').click(function() { - context.location = "/client#/band/setup/" + bandId + '/step1'; + context.location = "/client#/band/setup/" + bandId + '/step0'; return false; }); $("#btn-edit-band-info").unbind('click').click(function() { diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index d6cc2cd31..4788c7366 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -26,6 +26,52 @@ // TODO: Use a single variable for a mutually exclusive option: var step1, step2; var isSaving = false; + var currentStep = 0; + var STEPS_COUNT=5; + + function navBack() { + if (currentStep>0) { + saveBand(function() { + currentStep-- + renderCurrentPage() + }) + } + } + + function navCancel() { + resetForm() + window.history.go(-1) + return false + } + + function navNext() { + if (currentStep "19x19" h1#band-setup-title - | set up band + | my band = render "screen_navigation" .content-body .content-body-scroller form#band-setup-form div style="display:block;" - #band-setup-step-1.content-wrapper style="padding:10px 35px10px 35px;" - br + #band-setup-step-0.band-step.content-wrapper style="padding:10px 35px10px 35px;" + h2 edit profile: basics table width="100%" tr td colspan="2" - h2 | Step 1: General Information + tr#name_row td colspan="2" .band-photo.hidden @@ -65,21 +65,14 @@ / | Genres: / .band-setup-genres.w90 / table#band-genres[width="100%" cellpadding="10" cellspacing="6"] - - br clear="all" - .right - a#btn-band-setup-cancel.button-grey - | CANCEL - |    - a#btn-band-setup-next.button-orange - | SAVE & NEXT - .clearall - #band-setup-step-2.content-wrapper style="padding:10px 35px 10px 35px; display:none;" - br - h2 - span.band-setup-text-step2 - | Step 2: - | Add Band Members + #band-setup-step-1.band-step.content-wrapper + h2 edit profile: musical experience + #band-setup-step-2.band-step.content-wrapper + h2 edit profile: current interests + #band-setup-step-3.band-step.content-wrapper + h2 edit profile: online presence & performance samples + #band-setup-step-4.band-step.content-wrapper + h2 invite members br #band-setup-invite-musicians br @@ -105,14 +98,25 @@ = image_tag("content/icon_google.png", :size => "24x24", :align => "absmiddle") .right.mt5.ml5 | Google+ - br clear="all" - .right - a#btn-band-setup-back.button-grey - | BACK - |    - a#btn-band-setup-save.button-orange - | CREATE BAND - .clearall + / br clear="all" + / .right + / a#btn-band-setup-back.button-grey + / | BACK + / |    + / a#btn-band-setup-save.button-orange + / | CREATE BAND + / .clearall + + br clear="all" + .right + a#btn-band-setup-cancel.nav-button.button-grey + | CANCEL    + a#btn-band-setup-back.nav-button.button-grey.hidden + | BACK + a#btn-band-setup-next.nav-button.button-orange + | SAVE & NEXT + .clearall + script#template-band-setup-genres type="text/template" tr tr From a3c09bf8ad21e39d430acc050afc283e3597a519 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Wed, 20 May 2015 19:46:29 -0500 Subject: [PATCH 153/195] VRFS-3244 : Incremental - Refactor some common code. Whack unused code. --- web/app/assets/javascripts/band_setup.js | 189 +++-------------------- 1 file changed, 25 insertions(+), 164 deletions(-) diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 4788c7366..d8a8a51ee 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -141,7 +141,6 @@ removeErrors(); var band = buildBand(); - return rest.validateBand(band); } @@ -196,93 +195,24 @@ function saveBand(saveFn) { unbindNavButtons() var band = buildBand() - if (is_new_record()) { - rest.createBand(band) - .done(function (response) { - saveInvitations() - saveFn(band) - }) - .fail(function (jqXHR) { - app.notifyServerError(jqXHR, "Unable to create band") - }) - .always(function (jqXHR) { - bindNavButtons() - }) - } else { - rest.updateBand(band) - .done(function (response) { - saveInvitations() - saveFn(band) - }) - .fail(function (jqXHR) { - app.notifyServerError(jqXHR, "Unable to create band") - }) - .always(function (jqXHR) { - bindNavButtons() - }) - } - } - function saveBandOld() { - if (isSaving) return; - isSaving = true; - unbindNavButtons() - - var band = buildBand() - - if (is_new_record()) { - rest.createBand(band) - .done(function (response) { - isSaving = false; - if (0 < $('#selected-friends-band .invitation').length) { - createBandInvitations(response.id, function () { - showProfile(response.id); - }); - } else - showProfile(response.id); - }) - .fail(function (jqXHR) { - isSaving = false; + var saveBandFn = (is_new_record()) ? rest.createBand : rest.updateBand + saveBandFn(band) + .done(function (response) { + saveInvitations() + saveFn(band) + }) + .fail(function (jqXHR) { + if(jqXHR.status == 422) { + renderErrors(JSON.parse(jqXHR.responseText)) + } else { app.notifyServerError(jqXHR, "Unable to create band") - }); - ; - } - else { - band.id = bandId; - if (!step1 && !step2){ - rest.updateBand(band) - .done(function (response) { - isSaving = false; - createBandInvitations(band.id, function () { - showProfile(band.id); - }); - }).fail(function (jqXHR) { - isSaving = false; - app.notifyServerError(jqXHR, "Unable to create band") - }); - } else { - if (step1) { - rest.updateBand(band) - .done(function (response) { - isSaving = false; - app.notifyAlert('Band Information', 'Your changes have been saved'); - }).fail(function (jqXHR) { - isSaving = false; - app.notifyServerError(jqXHR, "Unable to update band") - }); - } else if (step2) { - isSaving = false; - if (0 < $('#selected-friends-band .invitation').length) { - createBandInvitations(bandId, function () { - app.notifyAlert('Band Members', 'Your invitations have been sent'); - showProfile(bandId); - }); - } else - showProfile(bandId); } - } - } - bindNavButtons(); + }) + .always(function (jqXHR) { + bindNavButtons() + }) + } } function createBandInvitations(bandId, onComplete) { @@ -313,7 +243,6 @@ function beforeShow(data) { inviteMusiciansUtil.clearSelections(); bandId = data.id == 'new' ? '' : data.id; - console.log("DDDDDDDD", data['d'], currentStep) currentStep=0 if (data['d']) { var stepNum = data['d'].substring(4) @@ -329,50 +258,23 @@ inviteMusiciansUtil.loadFriends(); if (!is_new_record()) { - //$("#band-setup-title").html("edit band"); - //$("#btn-band-setup-save").html("SAVE CHANGES"); $("#band-change-photo").html('Upload band photo.'); $('.band-photo').removeClass("hidden") // retrieve and initialize band profile data points - loadBandDetails(); - - // if (step2) { - // $("#band-setup-step-2").show(); - // $("#band-setup-step-1").hide(); - // $('.band-setup-text-step2').each(function(idx) { $(this).hide(); }); - // $('#btn-band-setup-back').text('CANCEL'); - // $('#btn-band-setup-save').text('SEND INVITATIONS'); - - // } else if (step1) { - // $("#band-setup-step-1").show(); - // $("#band-setup-step-2").hide(); - // $('.band-setup-text-step1').each(function(idx) { $(this).hide(); }); - // $('#btn-band-setup-next').text('SAVE & NEXT'); - // } - // if (! step1 && ! step2) { - // $('#btn-band-setup-next').text('SAVE & NEXT'); - // $('#btn-band-setup-back').text('CANCEL'); - // $('#btn-band-setup-save').text('CREATE BAND'); - // $('.band-setup-text-step1').each(function(idx) { $(this).show(); }); - // $('.band-setup-text-step2').each(function(idx) { $(this).show(); }); - // } - } - else { + loadBandDetails(); + } else { loadGenres(); - rest.getResolvedLocation() - .done(function (location) { - loadCountries(location.country, function () { - loadRegions(location.region, function () { - loadCities(location.city); - }); + // Load geo settings: + rest.getResolvedLocation().done(function (location) { + loadCountries(location.country, function () { + loadRegions(location.region, function () { + loadCities(location.city); }); }); + }); - - //$("#band-setup-title").html("set up band"); - //$("#btn-band-setup-save").html("CREATE BAND"); $('.band-photo').addClass("hidden") } renderCurrentPange() @@ -555,40 +457,6 @@ } function bindNavButtons() { - // $('#btn-band-setup-next').on("click", function (e) { - // e.stopPropagation() - // validateGeneralInfo() - // .done(function (response) { - // if (!step1 && !step2) { - // $("#band-setup-step-2").show(); - // $("#band-setup-step-1").hide(); - // } else if (step1) { - // saveBand(); - // step1=false - // step2=true - // } - // }) - // .fail(function (jqXHR) { - // if(jqXHR.status == 422) { - // renderErrors(JSON.parse(jqXHR.responseText)) - // } - // else { - // app.notifyServerError(jqXHR, "Unable to validate band") - // } - // }); - // return false; - // }); - - // $('#btn-band-setup-back').on("click", function () { - // if (!step2) { - // $("#band-setup-step-1").show(); - // $("#band-setup-step-2").hide(); - // } else { - // showProfile(bandId); - // return false; - // } - // }); - $('#btn-band-setup-back').on("click", function (e) { e.stopPropagation() navBack() @@ -601,7 +469,6 @@ return false }) - $('#btn-band-setup-next').on("click", function (e) { e.stopPropagation() navNext() @@ -622,7 +489,6 @@ $('#btn-band-setup-next').addClass("disabled") } - function events() { $('#selected-band-invitees').on("click", ".invitation a", removeInvitation); @@ -631,9 +497,7 @@ // friend input focus $('#band-invitee-input').focus(function () { $(this).val(''); - }); - - + }); $('#band-country').on('change', function (evt) { evt.stopPropagation(); @@ -684,8 +548,5 @@ this.initialize = initialize; this.afterShow = afterShow; return this; - }; - - - + }; })(window, jQuery); \ No newline at end of file From c5fc6cb93d52af2338b8484ae1a188bfdbc32b19 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Sat, 23 May 2015 08:36:48 -0500 Subject: [PATCH 154/195] Fix typos. --- web/app/assets/javascripts/band_setup.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index d8a8a51ee..311b3ef58 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -58,7 +58,7 @@ } } - function renderCurrentPange() { + function renderCurrentPage() { $(".band-step").addClass("hidden") $("#band-setup-step-" + currentStep).removeClass("hidden") if(currentStep==0) { @@ -211,8 +211,7 @@ }) .always(function (jqXHR) { bindNavButtons() - }) - } + }) } function createBandInvitations(bandId, onComplete) { @@ -277,7 +276,7 @@ $('.band-photo').addClass("hidden") } - renderCurrentPange() + renderCurrentPage() } function loadBandDetails() { From 064f402f593100418826089d15540e4ebc3c9199 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Sat, 23 May 2015 13:00:22 -0500 Subject: [PATCH 155/195] VRFS-3244 : Add additional fields for musical experience. Fix a few bugs and style issues. --- web/app/assets/javascripts/band_setup.js | 27 +- .../assets/stylesheets/client/band.css.scss | 33 ++- web/app/views/clients/_band_setup.html.slim | 259 ++++++++++-------- 3 files changed, 190 insertions(+), 129 deletions(-) diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 311b3ef58..a333019b0 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -22,7 +22,10 @@ var nilOptionText = 'n/a'; var bandId = ''; var friendInput=null; - + var bandType=null; + var bandStatus=null; + var concertCount=null; + // TODO: Use a single variable for a mutually exclusive option: var step1, step2; var isSaving = false; @@ -58,7 +61,7 @@ } } - function renderCurrentPage() { + function renderCurrentPage() { $(".band-step").addClass("hidden") $("#band-setup-step-" + currentStep).removeClass("hidden") if(currentStep==0) { @@ -73,8 +76,8 @@ } } - function is_new_record() { - return bandId.length == 0; + function is_new_record() { + return bandId==null || typeof(bandId)=='undefined' || bandId.length == 0; } function removeErrors() { @@ -110,8 +113,7 @@ resetGenres(); - $("#band-setup-step-1").show(); - $("#band-setup-step-2").hide(); + //renderCurrentPage();$(".band-step").addClass("hidden") $(friendInput) .unbind('blur') @@ -164,6 +166,7 @@ function buildBand() { var band = {}; + band.id = (is_new_record()) ? null : bandId; band.name = $("#band-name").val(); band.website = $("#band-website").val(); band.biography = $("#band-biography").val(); @@ -171,6 +174,10 @@ band.state = $("#band-region").val(); band.country = $("#band-country").val(); + band.band_type = bandType.val(); + band.band_status= bandStatus.val(); + band.concert_count= concertCount.val(); + if (step2) { band.genres = getSelectedGenres(); band.validate_genres = true @@ -285,6 +292,10 @@ $("#band-website").val(band.website); $("#band-biography").val(band.biography); + bandType.val(band.band_type) + bandStatus.val(band.band_status) + concertCount.val(band.concert_count) + if (band.photo_url) { $("#band-avatar").attr('src', band.photo_url); } @@ -541,6 +552,10 @@ 'afterShow': afterShow }; + bandType=$("#band-type") + bandStatus=$("#band-status") + concertCount=$("#concert-count") + app.bindScreen('band/setup', screenBindings); } diff --git a/web/app/assets/stylesheets/client/band.css.scss b/web/app/assets/stylesheets/client/band.css.scss index b2228bf5b..f3c291133 100644 --- a/web/app/assets/stylesheets/client/band.css.scss +++ b/web/app/assets/stylesheets/client/band.css.scss @@ -1,13 +1,15 @@ @import "client/common.css.scss"; #band-setup, #band-profile { - input, select, textarea { - @include border_box_sizing; - width: 100%; - padding: 2px 4px 2px 2px; + .band-field { + input, select, textarea { + @include border_box_sizing; + width: 100%; + padding: 2px 4px 2px 2px; + } } - td.band-biography { + td.band-biography, td.tdBandGenres { height:100%; vertical-align: top; #band-biography { @@ -18,7 +20,7 @@ .band-setup-genres { width:100%; - height:90px; + height:200px; background-color:#c5c5c5; border:none; -webkit-box-shadow: inset 2px 2px 3px 0px #888; @@ -346,12 +348,14 @@ width: 100%; } + #band-setup-form { padding: 0.25em 0.5em 0.25em 0.25em; - #band-setup-table { - + table.band-form-table { + width: 100%; + margin: 1em; } - + .easydropdown { padding: 0 3px; width:100%; @@ -369,4 +373,15 @@ margin-bottom:2px; } } +} + +.band-setup-genre { + input { + display: inline; + width: auto !important; + } + label { + display: inline; + width: auto; + } } \ No newline at end of file diff --git a/web/app/views/clients/_band_setup.html.slim b/web/app/views/clients/_band_setup.html.slim index 8f9c12451..37c44d525 100644 --- a/web/app/views/clients/_band_setup.html.slim +++ b/web/app/views/clients/_band_setup.html.slim @@ -7,126 +7,157 @@ = render "screen_navigation" .content-body .content-body-scroller - form#band-setup-form - div style="display:block;" - #band-setup-step-0.band-step.content-wrapper style="padding:10px 35px10px 35px;" - h2 edit profile: basics - table width="100%" - tr - td colspan="2" - - tr#name_row - td colspan="2" - .band-photo.hidden - span.field - a.band-avatar-profile href="#" - = image_tag "shared/avatar_generic_band.png", {:id => "band-avatar", :align=>"absmiddle", :height => 88, :width => 88 } - span.field - a#band-change-photo.small.ml20 href="#" - | Upload band photo. - - .band-name - .field - label for="band-name" - | Band Name: - input#band-name type="text" maxlength="1024" value="" - / td#tdBandWebsite[] - / .field - / label for="band-website" - / | Web Site: - / input#band-website[type="text" maxlength="4000" value=""] - tr - td.band-country - .field - label for="band-country" - | Country: - select#band-country - td.band-biography rowspan="3" - .field - label for="band-biography" - | Description / Bio: - textarea#band-biography rows="8" - tr - td.band-region - .field - label for="band-region" - | State/Region: - select#band-region - tr - td.band-city - .field - label for="band-city" - | City: - select#band-city - - / td#tdBandGenres[valign="top"] - / .field - / label for="band-genres" - / | Genres: - / .band-setup-genres.w90 - / table#band-genres[width="100%" cellpadding="10" cellspacing="6"] - #band-setup-step-1.band-step.content-wrapper - h2 edit profile: musical experience - #band-setup-step-2.band-step.content-wrapper - h2 edit profile: current interests - #band-setup-step-3.band-step.content-wrapper - h2 edit profile: online presence & performance samples - #band-setup-step-4.band-step.content-wrapper - h2 invite members - br - #band-setup-invite-musicians - br - br - | If your bandmates are not on JamKazam yet, use any of the options below to invite them to join the service. - br - br - .left.mr20 - .left - a.btn-email-invitation - = image_tag("content/icon_gmail.png", :size => "24x24", :align => "absmiddle") - .right.mt5.ml5 - | E-mail - .left.mr20 - .left - a.btn-facebook-invitation - = image_tag("content/icon_facebook.png", :size => "24x24", :align => "absmiddle") - .right.mt5.ml5 - | Facebook - .left.left - .left - a.btn-gmail-invitation - = image_tag("content/icon_google.png", :size => "24x24", :align => "absmiddle") - .right.mt5.ml5 - | Google+ - / br clear="all" - / .right - / a#btn-band-setup-back.button-grey - / | BACK - / |    - / a#btn-band-setup-save.button-orange - / | CREATE BAND - / .clearall + form#band-setup-form + #band-setup-step-0.band-step.content-wrapper + h2 edit profile: basics + table.band-form-table + tr + td colspan="2" - br clear="all" - .right - a#btn-band-setup-cancel.nav-button.button-grey - | CANCEL    - a#btn-band-setup-back.nav-button.button-grey.hidden - | BACK - a#btn-band-setup-next.nav-button.button-orange - | SAVE & NEXT - .clearall + tr#name_row + td colspan="2" + .band-photo.hidden + span.field + a.band-avatar-profile href="#" + = image_tag "shared/avatar_generic_band.png", {:id => "band-avatar", :align=>"absmiddle", :height => 88, :width => 88 } + span.field + a#band-change-photo.small.ml20 href="#" + | Upload band photo. + + .band-name + .field.band-field + label for="band-name" + | Band Name: + input#band-name type="text" maxlength="1024" value="" + / td#tdBandWebsite[] + / .field + / label for="band-website" + / | Web Site: + / input#band-website[type="text" maxlength="4000" value=""] + tr + td.band-country + .field.band-field + label for="band-country" + | Country: + select#band-country + td.band-biography rowspan="3" + .field.band-field + label for="band-biography" + | Description / Bio: + textarea#band-biography rows="8" + tr + td.band-region + .field.band-field + label for="band-region" + | State/Region: + select#band-region + tr + td.band-city + .field.band-field + label for="band-city" + | City: + select#band-city + + + #band-setup-step-1.band-step.content-wrapper + h2 edit profile: musical experience + + table.band-form-table + tr + td#tdBandGenres rowspan="3" + label for="band-genres" What genres do you play? + .band-setup-genres + table#band-genres + + td + .field + label for="band-type" Type + select#band-type.easydropdown name="band_type" + option value="" Not Specified + option value="virtual" Virtual + option value="physical" Physical + + tr + td + .field + label for="band-status" Status + select#band-status.easydropdown name="band_status" + option value="" Not Specified + option value="amateur" Amateur + option value="professional" Professional + + tr + td + .field + label for="concert-count" Concert Gigs Played + select#concert-count.easydropdown name="concert_count" + option value="" Not Specified + option value="0" Zero + option value="1" Under 10 + option value="2" 10 to 50 + option value="3" 50 to 100 + option value="4" Over 100 + + #band-setup-step-2.band-step.content-wrapper + h2 edit profile: current interests + #band-setup-step-3.band-step.content-wrapper + h2 edit profile: online presence & performance samples + #band-setup-step-4.band-step.content-wrapper + h2 invite members + br + #band-setup-invite-musicians + br + br + | If your bandmates are not on JamKazam yet, use any of the options below to invite them to join the service. + br + br + .left.mr20 + .left + a.btn-email-invitation + = image_tag("content/icon_gmail.png", :size => "24x24", :align => "absmiddle") + .right.mt5.ml5 + | E-mail + .left.mr20 + .left + a.btn-facebook-invitation + = image_tag("content/icon_facebook.png", :size => "24x24", :align => "absmiddle") + .right.mt5.ml5 + | Facebook + .left.left + .left + a.btn-gmail-invitation + = image_tag("content/icon_google.png", :size => "24x24", :align => "absmiddle") + .right.mt5.ml5 + | Google+ + / br clear="all" + / .right + / a#btn-band-setup-back.button-grey + / | BACK + / |    + / a#btn-band-setup-save.button-orange + / | CREATE BAND + / .clearall + + br clear="all" + .right + a#btn-band-setup-cancel.nav-button.button-grey + | CANCEL    + a#btn-band-setup-back.nav-button.button-grey.hidden + | BACK + a#btn-band-setup-next.nav-button.button-orange + | SAVE & NEXT + .clearall script#template-band-setup-genres type="text/template" - tr + tr.band-setup-genre tr - td - input value="{id}" {checked} type="checkbox" - | {description} + td + {description} + script#template-band-invitation type="text/template" - .invitation user-id="{userId}" - | {userName} + .invitation user-id="{{userId}}" + | {{userName}} a img src="shared/icon_delete_sm.png" width="13" height="13" From 79af533348c0b1a3ce28f5d35038e5b44d070c0e Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Tue, 26 May 2015 21:17:25 -0500 Subject: [PATCH 156/195] VRFS-3245 : Layout and initial styling of band profile current interests screen -- incremental. --- .../assets/stylesheets/client/band.css.scss | 38 +++++++--- web/app/views/clients/_band_setup.html.slim | 72 +++++++++++++++++++ 2 files changed, 102 insertions(+), 8 deletions(-) diff --git a/web/app/assets/stylesheets/client/band.css.scss b/web/app/assets/stylesheets/client/band.css.scss index f3c291133..c880af36c 100644 --- a/web/app/assets/stylesheets/client/band.css.scss +++ b/web/app/assets/stylesheets/client/band.css.scss @@ -1,14 +1,25 @@ @import "client/common.css.scss"; #band-setup, #band-profile { + font-family: helvetica, verdana, arial, sans-serif; .band-field { - input, select, textarea { + input[type="text"], select, textarea { @include border_box_sizing; width: 100%; padding: 2px 4px 2px 2px; } } + .radio-field { + display: inline; + label { + display: inline !important; + } + input { + display: inline !important; + } + } + td.band-biography, td.tdBandGenres { height:100%; vertical-align: top; @@ -373,15 +384,26 @@ margin-bottom:2px; } } -} -.band-setup-genre { - input { - display: inline; - width: auto !important; + + .band-setup-genre { + input { + display: inline; + width: auto !important; + } + label { + display: inline; + width: auto; + } } + label { - display: inline; - width: auto; + font-size: 1.1em; } + + label.strong-label { + font-weight: bold; + font-size: 1.2em; + } + } \ No newline at end of file diff --git a/web/app/views/clients/_band_setup.html.slim b/web/app/views/clients/_band_setup.html.slim index 37c44d525..1fb0eba8b 100644 --- a/web/app/views/clients/_band_setup.html.slim +++ b/web/app/views/clients/_band_setup.html.slim @@ -100,8 +100,79 @@ #band-setup-step-2.band-step.content-wrapper h2 edit profile: current interests + table.band-form-table + tr + td + label.strong-label for="new-member" + | We want to add a new member   + a.help help-topic="profile-interests-virtual-band" [?] + td + label for="desired-experience" + | Desired Experience   + a.select-desired-experience select + td + label for="play-commitment" Play Commitment + td + label for="touring-option" Touring Option + tr + td + .radio-field + input#new-member-yes.irad_minimal type="radio" name="new-member" value='true' + label for='new-member-yes' Yes + .radio-field + input#new-member-no.irad_minimal type="radio" name="new-member" value='false' + label for='new-member-no' No + td + em + #desired-experience-label None specified + td + select#play-commitment.easydropdown name="play_commitment" + option value="1" Infrequent + option value="2" Once a Week + option value="3" 2-3 Times Per Week + option value="4" 4+ Times Per Week + td + select#touring-option.easydropdown name="touring_option" + option value="true" Yes + option value="false" No + tr + td + label.strong-label for="paid-gigs" + | We want to play paid gigs   + a.help help-topic="profile-interests-paid-gigs" [?] + td + label for="hourly-rate" Hourly Rate: + td + label for="gig-minimum" Gig Minimum: + tr + td + .radio-field + input#paid-gigs-yes.irad_minimal type="radio" name="paid_gigs" value='true' + label for="paid-gigs-yes" Yes + .radio-field + input#paid-gigs-no.irad_minimal type="radio" name="paid_gigs" value='false' + label for="paid-gigs-no" No + td + input#hourly-rate type="number" name="hourly_rate" + td + input#gig-minimum type="number" name="gig_minimum" + tr + td + label.strong-label for="free-gigs" + | We want to play free gigs   + a.help help-topic="profile-interests-free-gigs" [?] + tr + td + .radio-field + input#free-gigs-yes.irad_minimal type="radio" name="free-gigs" value='true' + label for="free-gigs-yes" Yes + .radio-field + input#free-gigs-no.irad_minimal type="radio" name="free-gigs" value='false' + label for="free-gigs-no" No + #band-setup-step-3.band-step.content-wrapper h2 edit profile: online presence & performance samples + table.band-form-table #band-setup-step-4.band-step.content-wrapper h2 invite members br @@ -148,6 +219,7 @@ | SAVE & NEXT .clearall + script#template-band-setup-genres type="text/template" tr.band-setup-genre tr From c714b98af137e122b8902311ae938a9f2b125862 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Wed, 27 May 2015 13:11:12 -0500 Subject: [PATCH 157/195] VRFS-3245 : Integrate iCheck and style accordingly. --- web/app/assets/javascripts/band_setup.js | 7 ++++++ .../assets/stylesheets/client/band.css.scss | 15 +++++++----- web/app/views/clients/_band_setup.html.slim | 24 +++++++++---------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index a333019b0..58d84e0d9 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -557,6 +557,13 @@ concertCount=$("#concert-count") app.bindScreen('band/setup', screenBindings); + + $('input[type=radio]') + .iCheck({ + checkboxClass: 'icheckbox_minimal', + radioClass: 'iradio_minimal', + inheritClass: true + }); } this.initialize = initialize; diff --git a/web/app/assets/stylesheets/client/band.css.scss b/web/app/assets/stylesheets/client/band.css.scss index c880af36c..322880bba 100644 --- a/web/app/assets/stylesheets/client/band.css.scss +++ b/web/app/assets/stylesheets/client/band.css.scss @@ -1,7 +1,7 @@ @import "client/common.css.scss"; #band-setup, #band-profile { - font-family: helvetica, verdana, arial, sans-serif; + font-family: Raleway, Arial, Helvetica, verdana, arial, sans-serif; .band-field { input[type="text"], select, textarea { @include border_box_sizing; @@ -12,11 +12,14 @@ .radio-field { display: inline; - label { - display: inline !important; + padding: 2px; + margin: 0.5em 2em 0.5em 0.25em; + label { + display: inline; } - input { - display: inline !important; + .iradio-inline { + display: inline-block; + //padding: 2px; } } @@ -361,7 +364,7 @@ #band-setup-form { - padding: 0.25em 0.5em 0.25em 0.25em; + padding: 0.25em 0.5em 1.25em 0.25em; table.band-form-table { width: 100%; margin: 1em; diff --git a/web/app/views/clients/_band_setup.html.slim b/web/app/views/clients/_band_setup.html.slim index 1fb0eba8b..4b1fcc137 100644 --- a/web/app/views/clients/_band_setup.html.slim +++ b/web/app/views/clients/_band_setup.html.slim @@ -116,11 +116,11 @@ label for="touring-option" Touring Option tr td - .radio-field - input#new-member-yes.irad_minimal type="radio" name="new-member" value='true' + .radio-field + input#new-member-yes.iradio-inline type="radio" name="new-member" value='true' label for='new-member-yes' Yes - .radio-field - input#new-member-no.irad_minimal type="radio" name="new-member" value='false' + .radio-field + input#new-member-no.iradio-inline type="radio" name="new-member" value='false' label for='new-member-no' No td em @@ -146,11 +146,11 @@ label for="gig-minimum" Gig Minimum: tr td - .radio-field - input#paid-gigs-yes.irad_minimal type="radio" name="paid_gigs" value='true' + .radio-field + input#paid-gigs-yes.iradio-inline type="radio" name="paid_gigs" value='true' label for="paid-gigs-yes" Yes - .radio-field - input#paid-gigs-no.irad_minimal type="radio" name="paid_gigs" value='false' + .radio-field + input#paid-gigs-no.iradio-inline type="radio" name="paid_gigs" value='false' label for="paid-gigs-no" No td input#hourly-rate type="number" name="hourly_rate" @@ -163,11 +163,11 @@ a.help help-topic="profile-interests-free-gigs" [?] tr td - .radio-field - input#free-gigs-yes.irad_minimal type="radio" name="free-gigs" value='true' + .radio-field + input#free-gigs-yes.iradio-inline type="radio" name="free-gigs" value='true' label for="free-gigs-yes" Yes - .radio-field - input#free-gigs-no.irad_minimal type="radio" name="free-gigs" value='false' + .radio-field + input#free-gigs-no.iradio-inline type="radio" name="free-gigs" value='false' label for="free-gigs-no" No #band-setup-step-3.band-step.content-wrapper From 9d59e53679f9b39d035383088a7c277f4ee93796 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Wed, 27 May 2015 16:14:00 -0500 Subject: [PATCH 158/195] VRFS-3245 : Tweak style/layout to more closely match spec. --- .../assets/stylesheets/client/band.css.scss | 20 +++++++++++++++++++ web/app/views/clients/_band_setup.html.slim | 3 +-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/web/app/assets/stylesheets/client/band.css.scss b/web/app/assets/stylesheets/client/band.css.scss index 322880bba..7d76277b3 100644 --- a/web/app/assets/stylesheets/client/band.css.scss +++ b/web/app/assets/stylesheets/client/band.css.scss @@ -8,6 +8,17 @@ width: 100%; padding: 2px 4px 2px 2px; } + + } + + // Mimic style of easydropdown selects: + input[type="number"] { + border-radius: 6px; + background-color: #c5c5c5; + border-right-style: outset; + border-bottom-style: outset; + height: 15px; + padding: 5px; } .radio-field { @@ -16,6 +27,7 @@ margin: 0.5em 2em 0.5em 0.25em; label { display: inline; + padding: 2px; } .iradio-inline { display: inline-block; @@ -23,6 +35,10 @@ } } + tr:nth-child(even) td { + padding-bottom: 1em; + } + td.band-biography, td.tdBandGenres { height:100%; vertical-align: top; @@ -386,6 +402,10 @@ label { margin-bottom:2px; } + + #desired-experience-label { + font-style: italic; + } } diff --git a/web/app/views/clients/_band_setup.html.slim b/web/app/views/clients/_band_setup.html.slim index 4b1fcc137..3461d98bc 100644 --- a/web/app/views/clients/_band_setup.html.slim +++ b/web/app/views/clients/_band_setup.html.slim @@ -123,8 +123,7 @@ input#new-member-no.iradio-inline type="radio" name="new-member" value='false' label for='new-member-no' No td - em - #desired-experience-label None specified + #desired-experience-label None specified td select#play-commitment.easydropdown name="play_commitment" option value="1" Infrequent From 8d73ef6aaca3210ba9bc44553f0ee89a649d9a3c Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Thu, 28 May 2015 14:21:55 -0500 Subject: [PATCH 159/195] VRFS-3245 : Serialize additional attributes. --- web/app/assets/javascripts/band_setup.js | 58 +++++++++++++++++---- web/app/views/api_bands/show.rabl | 4 +- web/app/views/clients/_band_setup.html.slim | 16 +++--- 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 58d84e0d9..4798354a1 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -24,11 +24,7 @@ var friendInput=null; var bandType=null; var bandStatus=null; - var concertCount=null; - - // TODO: Use a single variable for a mutually exclusive option: - var step1, step2; - var isSaving = false; + var concertCount=null; var currentStep = 0; var STEPS_COUNT=5; @@ -73,7 +69,7 @@ } else { $("#btn-band-setup-back").removeClass("hidden") $("#btn-band-setup-next").removeClass("hidden").html("SAVE & FINISH") - } + } } function is_new_record() { @@ -111,6 +107,15 @@ // website $('#band-website').val(''); + $("#new-member-no").iCheck('check').attr('checked', 'checked') + $("#paid-gigs-no").iCheck('check').attr('checked', 'checked') + $("#free-gigs-no").iCheck('check').attr('checked', 'checked') + $('#touring-option').val('no') + + + $("#play-commitment").val('1') + $("#hourly-rate").val("0.0") + $("#gig-minimum").val("0.0") resetGenres(); //renderCurrentPage();$(".band-step").addClass("hidden") @@ -178,7 +183,16 @@ band.band_status= bandStatus.val(); band.concert_count= concertCount.val(); - if (step2) { + band.add_new_members = $('input[name="add_new_members"]:checked').val()=="yes" + band.paid_gigs = $('input[name="paid_gigs"]:checked').val()=="yes" + band.free_gigs=$('input[name="free_gigs"]:checked').val()=="yes" + band.touring_option=$('#touring-option').val()=="yes" + + band.play_commitment=$("#play-commitment").val() + band.hourly_rate=$("#hourly-rate").val() + band.gig_minimum=$("#gig-minimum").val() + + if (currentStep==3) { band.genres = getSelectedGenres(); band.validate_genres = true } else { @@ -295,7 +309,30 @@ bandType.val(band.band_type) bandStatus.val(band.band_status) concertCount.val(band.concert_count) + if (band.add_new_members){ + $("#new-member-no").iCheck('check').attr('checked', 'checked') + } else { + $("#new-member-yes").iCheck('check').attr('checked', 'checked') + } + if (band.paid_gigs) { + $("#paid-gigs-no").iCheck('check').attr('checked', 'checked') + } else { + $("#paid-gigs-yes").iCheck('check').attr('checked', 'checked') + } + + if (band.free_gigs) { + $("#free-gigs-no").iCheck('check').attr('checked', 'checked') + } else { + $("#free-gigs-yes").iCheck('check').attr('checked', 'checked') + } + + $('#touring-option').val(band.touring_option ? 'yes' : 'no') + $("#play-commitment").val(band.play_commitment) + $("#hourly-rate").val(band.hourly_rate) + $("#gig-minimum").val(band.gig_minimum) + + // Initialize avatar if (band.photo_url) { $("#band-avatar").attr('src', band.photo_url); } @@ -307,8 +344,8 @@ loadCities(band.city); }); }); + ; - // TODO: initialize avatar }); } @@ -448,8 +485,7 @@ $('#selected-band-invitees').append(invitationHtml); $('#band-invitee-input').select(); selectedFriendIds[data] = true; - } - else { + } else { $('#band-invitee-input').select(); context.alert('Invitation already exists for this musician.'); } @@ -536,7 +572,7 @@ $('div[layout-id="band/setup"] .btn-facebook-invitation').click(function () { invitationDialog.showFacebookDialog(); }); - + $(friendInput).focus(function() { $(this).val(''); }) } diff --git a/web/app/views/api_bands/show.rabl b/web/app/views/api_bands/show.rabl index 74d8dd08d..d398dbbd3 100644 --- a/web/app/views/api_bands/show.rabl +++ b/web/app/views/api_bands/show.rabl @@ -1,7 +1,9 @@ object @band attributes :id, :name, :city, :state, :country, :location, :website, :biography, :photo_url, :logo_url, :liker_count, :follower_count, :recording_count, :session_count, -:original_fpfile_photo, :cropped_fpfile_photo, :crop_selection_photo +:original_fpfile_photo, :cropped_fpfile_photo, :crop_selection_photo, +:band_type, :band_status, :concert_count, :add_new_members, :play_commitment, :touring_option, :paid_gigs, +:free_gigs, :hourly_rate, :gig_minimum child :users => :musicians do attributes :id, :first_name, :last_name, :name, :photo_url diff --git a/web/app/views/clients/_band_setup.html.slim b/web/app/views/clients/_band_setup.html.slim index 3461d98bc..85a9321e8 100644 --- a/web/app/views/clients/_band_setup.html.slim +++ b/web/app/views/clients/_band_setup.html.slim @@ -117,10 +117,10 @@ tr td .radio-field - input#new-member-yes.iradio-inline type="radio" name="new-member" value='true' + input#new-member-yes.iradio-inline type="radio" name="add_new_members" value='yes' label for='new-member-yes' Yes .radio-field - input#new-member-no.iradio-inline type="radio" name="new-member" value='false' + input#new-member-no.iradio-inline type="radio" name="add_new_members" value='no' label for='new-member-no' No td #desired-experience-label None specified @@ -132,8 +132,8 @@ option value="4" 4+ Times Per Week td select#touring-option.easydropdown name="touring_option" - option value="true" Yes - option value="false" No + option value="yes" Yes + option value="no" No tr td label.strong-label for="paid-gigs" @@ -146,10 +146,10 @@ tr td .radio-field - input#paid-gigs-yes.iradio-inline type="radio" name="paid_gigs" value='true' + input#paid-gigs-yes.iradio-inline type="radio" name="paid_gigs" value='yes' label for="paid-gigs-yes" Yes .radio-field - input#paid-gigs-no.iradio-inline type="radio" name="paid_gigs" value='false' + input#paid-gigs-no.iradio-inline type="radio" name="paid_gigs" value='no' label for="paid-gigs-no" No td input#hourly-rate type="number" name="hourly_rate" @@ -163,10 +163,10 @@ tr td .radio-field - input#free-gigs-yes.iradio-inline type="radio" name="free-gigs" value='true' + input#free-gigs-yes.iradio-inline type="radio" name="free_gigs" value='yes' label for="free-gigs-yes" Yes .radio-field - input#free-gigs-no.iradio-inline type="radio" name="free-gigs" value='false' + input#free-gigs-no.iradio-inline type="radio" name="free_gigs" value='no' label for="free-gigs-no" No #band-setup-step-3.band-step.content-wrapper From a4317a532bf7f555ff0202ad2aa996ce9d93bdb2 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Thu, 28 May 2015 17:12:39 -0500 Subject: [PATCH 160/195] VRFS-3245 : Render optional controls * If new member is wanted, show play commitment and touring option * If paid gigs, show amount and gig minimum * Works largely through class names. --- web/app/assets/javascripts/band_setup.js | 37 ++++++++++++++++----- web/app/views/clients/_band_setup.html.slim | 28 ++++++++-------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 4798354a1..eadf82a99 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -69,10 +69,30 @@ } else { $("#btn-band-setup-back").removeClass("hidden") $("#btn-band-setup-next").removeClass("hidden").html("SAVE & FINISH") - } + } + renderOptionalControls() } - function is_new_record() { + function renderOptionalControls(e) { + if(e){e.stopPropagation()} + + // Is new member selected? + if ($('input[name="add_new_members"]:checked').val()=="yes") { + $(".new-member-dependent").removeClass("hidden") + } else { + $(".new-member-dependent").addClass("hidden") + } + + // Is paid gigs selected? + if ($('input[name="paid_gigs"]:checked').val()=="yes") { + $(".paid-gigs-dependent").removeClass("hidden") + } else { + $(".paid-gigs-dependent").addClass("hidden") + } + return false; + } + + function isNewBand() { return bandId==null || typeof(bandId)=='undefined' || bandId.length == 0; } @@ -118,8 +138,6 @@ $("#gig-minimum").val("0.0") resetGenres(); - //renderCurrentPage();$(".band-step").addClass("hidden") - $(friendInput) .unbind('blur') .attr("placeholder", "Looking up friends...") @@ -171,7 +189,7 @@ function buildBand() { var band = {}; - band.id = (is_new_record()) ? null : bandId; + band.id = (isNewBand()) ? null : bandId; band.name = $("#band-name").val(); band.website = $("#band-website").val(); band.biography = $("#band-biography").val(); @@ -217,7 +235,7 @@ unbindNavButtons() var band = buildBand() - var saveBandFn = (is_new_record()) ? rest.createBand : rest.updateBand + var saveBandFn = (isNewBand()) ? rest.createBand : rest.updateBand saveBandFn(band) .done(function (response) { saveInvitations() @@ -277,7 +295,7 @@ function afterShow(data) { inviteMusiciansUtil.loadFriends(); - if (!is_new_record()) { + if (!isNewBand()) { $("#band-change-photo").html('Upload band photo.'); $('.band-photo').removeClass("hidden") @@ -344,7 +362,8 @@ loadCities(band.city); }); }); - ; + + renderOptionalControls(); }); } @@ -572,6 +591,8 @@ $('div[layout-id="band/setup"] .btn-facebook-invitation').click(function () { invitationDialog.showFacebookDialog(); }); + + $('#band-setup').on('ifToggled', 'input[type="radio"].dependent-master', renderOptionalControls); $(friendInput).focus(function() { $(this).val(''); }) } diff --git a/web/app/views/clients/_band_setup.html.slim b/web/app/views/clients/_band_setup.html.slim index 85a9321e8..b869dcd36 100644 --- a/web/app/views/clients/_band_setup.html.slim +++ b/web/app/views/clients/_band_setup.html.slim @@ -106,31 +106,31 @@ label.strong-label for="new-member" | We want to add a new member   a.help help-topic="profile-interests-virtual-band" [?] - td + td.new-member-dependent label for="desired-experience" | Desired Experience   a.select-desired-experience select - td + td.new-member-dependent label for="play-commitment" Play Commitment - td + td.new-member-dependent label for="touring-option" Touring Option tr td .radio-field - input#new-member-yes.iradio-inline type="radio" name="add_new_members" value='yes' + input#new-member-yes.iradio-inline.dependent-master type="radio" name="add_new_members" value='yes' label for='new-member-yes' Yes .radio-field - input#new-member-no.iradio-inline type="radio" name="add_new_members" value='no' + input#new-member-no.iradio-inline.dependent-master type="radio" name="add_new_members" value='no' label for='new-member-no' No - td + td.new-member-dependent #desired-experience-label None specified - td + td.new-member-dependent select#play-commitment.easydropdown name="play_commitment" option value="1" Infrequent option value="2" Once a Week option value="3" 2-3 Times Per Week option value="4" 4+ Times Per Week - td + td.new-member-dependent select#touring-option.easydropdown name="touring_option" option value="yes" Yes option value="no" No @@ -139,21 +139,21 @@ label.strong-label for="paid-gigs" | We want to play paid gigs   a.help help-topic="profile-interests-paid-gigs" [?] - td + td.paid-gigs-dependent label for="hourly-rate" Hourly Rate: - td + td.paid-gigs-dependent label for="gig-minimum" Gig Minimum: tr td .radio-field - input#paid-gigs-yes.iradio-inline type="radio" name="paid_gigs" value='yes' + input#paid-gigs-yes.iradio-inline.dependent-master type="radio" name="paid_gigs" value='yes' label for="paid-gigs-yes" Yes .radio-field - input#paid-gigs-no.iradio-inline type="radio" name="paid_gigs" value='no' + input#paid-gigs-no.iradio-inline.dependent-master type="radio" name="paid_gigs" value='no' label for="paid-gigs-no" No - td + td.paid-gigs-dependent input#hourly-rate type="number" name="hourly_rate" - td + td.paid-gigs-dependent input#gig-minimum type="number" name="gig_minimum" tr td From c02f8ceda8f8d6629b4590013e8e3a1de3c42349 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Thu, 28 May 2015 22:39:32 -0500 Subject: [PATCH 161/195] VRFS-3245 : Fix reset form logic. --- web/app/assets/javascripts/band_setup.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index eadf82a99..252039d2d 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -126,7 +126,7 @@ // website $('#band-website').val(''); - + $("#new-member-no").iCheck('check').attr('checked', 'checked') $("#paid-gigs-no").iCheck('check').attr('checked', 'checked') $("#free-gigs-no").iCheck('check').attr('checked', 'checked') @@ -286,10 +286,10 @@ var stepNum = data['d'].substring(4) if(stepNum) { currentStep=stepNum - delete data['d']; - resetForm(); + delete data['d']; } } + resetForm(); } function afterShow(data) { @@ -327,6 +327,7 @@ bandType.val(band.band_type) bandStatus.val(band.band_status) concertCount.val(band.concert_count) + if (band.add_new_members){ $("#new-member-no").iCheck('check').attr('checked', 'checked') } else { From dbd161a068501ec24dea0ef825d8fbce928949a5 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Thu, 28 May 2015 22:40:19 -0500 Subject: [PATCH 162/195] VRFS-3245 : Change "edit profile" to "set up band". --- web/app/views/clients/_band_setup.html.slim | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web/app/views/clients/_band_setup.html.slim b/web/app/views/clients/_band_setup.html.slim index b869dcd36..694eec75e 100644 --- a/web/app/views/clients/_band_setup.html.slim +++ b/web/app/views/clients/_band_setup.html.slim @@ -9,7 +9,7 @@ .content-body-scroller form#band-setup-form #band-setup-step-0.band-step.content-wrapper - h2 edit profile: basics + h2 set up band: basics table.band-form-table tr td colspan="2" @@ -60,7 +60,7 @@ #band-setup-step-1.band-step.content-wrapper - h2 edit profile: musical experience + h2 set up band: musical experience table.band-form-table tr @@ -99,7 +99,7 @@ option value="4" Over 100 #band-setup-step-2.band-step.content-wrapper - h2 edit profile: current interests + h2 set up band: current interests table.band-form-table tr td @@ -170,7 +170,7 @@ label for="free-gigs-no" No #band-setup-step-3.band-step.content-wrapper - h2 edit profile: online presence & performance samples + h2 set up band: online presence & performance samples table.band-form-table #band-setup-step-4.band-step.content-wrapper h2 invite members From d91d0b0cfe14cf987bdf61e2f521ab5159b62035 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Thu, 28 May 2015 22:41:03 -0500 Subject: [PATCH 163/195] VRFS-3245 : Instrument selector dialog -- incremental. --- .../dialog/instrumentSelectorDialog.js | 83 +++++++++++++++++++ web/app/assets/javascripts/ui_helper.js | 6 ++ web/app/views/dialogs/_dialogs.html.haml | 1 + .../_instrumentSelectorDialog.html.haml | 16 ++++ 4 files changed, 106 insertions(+) create mode 100644 web/app/assets/javascripts/dialog/instrumentSelectorDialog.js create mode 100644 web/app/views/dialogs/_instrumentSelectorDialog.html.haml diff --git a/web/app/assets/javascripts/dialog/instrumentSelectorDialog.js b/web/app/assets/javascripts/dialog/instrumentSelectorDialog.js new file mode 100644 index 000000000..b77c6fc1b --- /dev/null +++ b/web/app/assets/javascripts/dialog/instrumentSelectorDialog.js @@ -0,0 +1,83 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.InstrumentSelectorDialog = function(app, type, instruments, callback) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var $dialog = null; + var dialogId = 'instrument-selector-dialog'; + var $screen = $('#' + dialogId); + var $btnSelect = $screen.find(".btn-select-instruments"); + var $instructions = $screen.find('.instructions'); + var $instruments = $screen.find('.instruments'); + + function beforeShow(data) { + } + + function afterShow(data) { + var instrumentList = context.JK.instruments; + + $instruments.empty(); + + if (instrumentList) { + $.each(instrumentList, function(index, val) { + $instruments.append('
  • '); + var checked = ''; + if (instruments && $.inArray(val.id, instruments) > -1) { + checked = 'checked'; + } + + $instruments.append('' + val.description); + $instruments.append('
  • '); + }); + } + } + + function afterHide() { + $btnSelect.unbind("click") + } + + function showDialog() { + return app.layout.showDialog(dialogId); + } + + function events() { + $btnSelect.unbind("click").bind("click", function(evt) { + evt.preventDefault(); + var selectedInstruments = []; + $instruments.find('input[type=checkbox]:checked').each(function(index) { + selectedInstruments.push($(this).val()); + }); + + if (callback) { + callback(selectedInstruments); + } + + app.layout.closeDialog(dialogId); + + return false; + + }); + } + + function initialize() { + var dialogBindings = { + 'beforeShow' : beforeShow, + 'afterShow' : afterShow, + 'afterHide': afterHide + }; + + app.bindDialog(dialogId, dialogBindings); + + $instructions.html('Select one or more instruments for ' + type + ':'); + + events(); + } + + this.initialize = initialize; + this.showDialog = showDialog; + } + + return this; +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/ui_helper.js b/web/app/assets/javascripts/ui_helper.js index 01d2aa403..c68ed7c09 100644 --- a/web/app/assets/javascripts/ui_helper.js +++ b/web/app/assets/javascripts/ui_helper.js @@ -62,6 +62,12 @@ }); } + function launchInstrumentSelectorDialog(type, instruments, callback) { + var instrumentSelectorDialog = new JK.InstrumentSelectorDialog(JK.app, type, instruments, callback); + instrumentSelectorDialog.initialize(); + return instrumentSelectorDialog.showDialog(); + } + function launchGenreSelectorDialog(type, genres, callback) { var genreSelectorDialog = new JK.GenreSelectorDialog(JK.app, type, genres, callback); genreSelectorDialog.initialize(); diff --git a/web/app/views/dialogs/_dialogs.html.haml b/web/app/views/dialogs/_dialogs.html.haml index 8f2905797..6f29cde19 100644 --- a/web/app/views/dialogs/_dialogs.html.haml +++ b/web/app/views/dialogs/_dialogs.html.haml @@ -38,6 +38,7 @@ = render 'dialogs/loginRequiredDialog' = render 'dialogs/jamtrackPaymentHistoryDialog' = render 'dialogs/singlePlayerProfileGuard' += render 'dialogs/instrumentSelectorDialog' = render 'dialogs/genreSelectorDialog' = render 'dialogs/recordingSelectorDialog' = render 'dialogs/soundCloudPlayerDialog' \ No newline at end of file diff --git a/web/app/views/dialogs/_instrumentSelectorDialog.html.haml b/web/app/views/dialogs/_instrumentSelectorDialog.html.haml new file mode 100644 index 000000000..ac6e44c2d --- /dev/null +++ b/web/app/views/dialogs/_instrumentSelectorDialog.html.haml @@ -0,0 +1,16 @@ +.dialog.dialog-overlay-sm{layout: 'dialog', 'layout-id' => 'instrument-selector-dialog', id: 'instrument-selector-dialog'} + .content-head + = image_tag "content/icon_checkmark_circle.png", {:width => 20, :height => 20, :class => 'content-icon' } + %h1 + = 'select instrument' + .dialog-inner + %strong + .instructions + %br{:clear => "all"}/ + .content-body + .content-body-scroller + %ul.instruments.three-column-list-container + + .right.action-buttons + %a.button-grey.btn-cancel-dialog{'layout-action' => 'cancel'} CANCEL + %a.button-orange.btn-select-instruments SELECT \ No newline at end of file From 540c3cc37f26b89b8606037014f6557ce2a13eee Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Fri, 29 May 2015 21:04:33 -0500 Subject: [PATCH 164/195] VRFS-3245 : Instrument selector * Wrap instrument selector widget in dialog widget and delegate as necessary * Add instrument info to band api result * Integrate with band setup flow * Style to approximate spec * Fix a few logic and syntax errors encountered along the way --- web/app/assets/javascripts/band_setup.js | 55 ++++++++++++++++++- .../dialog/instrumentSelectorDialog.js | 49 ++++++++--------- .../assets/javascripts/instrumentSelector.js | 23 ++++---- web/app/assets/javascripts/ui_helper.js | 1 + .../dialogs/instrumentSelectorDialog.css.scss | 44 +++++++++++++++ web/app/views/api_bands/show.rabl | 4 ++ web/app/views/clients/_band_setup.html.slim | 2 +- .../_instrumentSelectorDialog.html.haml | 5 +- .../views/users/_instrument_selector.html.erb | 2 +- 9 files changed, 143 insertions(+), 42 deletions(-) create mode 100644 web/app/assets/stylesheets/dialogs/instrumentSelectorDialog.css.scss diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 252039d2d..15a869dc1 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -9,6 +9,8 @@ // accounts_profiles.js context.JK.BandSetupScreen = function (app) { + var NONE_SPECIFIED = 'None specified' + var ui = new context.JK.UIHelper(JK.app) var logger = context.JK.logger; var rest = context.JK.Rest(); var inviteMusiciansUtil = null; @@ -27,6 +29,7 @@ var concertCount=null; var currentStep = 0; var STEPS_COUNT=5; + var $selectedInstruments=[] function navBack() { if (currentStep>0) { @@ -136,7 +139,8 @@ $("#play-commitment").val('1') $("#hourly-rate").val("0.0") $("#gig-minimum").val("0.0") - resetGenres(); + resetGenres(); + renderDesiredExperienceLabel([]) $(friendInput) .unbind('blur') @@ -188,7 +192,7 @@ } function buildBand() { - var band = {}; + var band = {instruments:[]}; band.id = (isNewBand()) ? null : bandId; band.name = $("#band-name").val(); band.website = $("#band-website").val(); @@ -216,9 +220,29 @@ } else { band.validate_genres = false } + + + $.each($selectedInstruments, function(index, instrument) { + var h = {} + h.instrument_id = instrument.id + h.proficiency_level = instrument.level + band.instruments.push(h) + }) + return band; } + function renderDesiredExperienceLabel(selectedInstruments) { + $selectedInstruments=selectedInstruments + var instrumentText="" + $.each($selectedInstruments, function(index, instrument) { + if (instrumentText.length!=0) {instrumentText += ", "} + instrumentText += instrument.name + }) + + $("#desired-experience-label").html(($selectedInstruments && $selectedInstruments.length > 0) ? instrumentText : NONE_SPECIFIED) + } + function showProfile(band_id) { context.location = "/client#/bandProfile/" + band_id; } @@ -318,6 +342,11 @@ renderCurrentPage() } + function loadDesiredExperience() { + + } + + function loadBandDetails() { rest.getBand(bandId).done(function (band) { $("#band-name").val(band.name); @@ -366,6 +395,16 @@ renderOptionalControls(); + $.each(band.instruments, function(index, instrument) { + var h = {} + h.id = instrument.instrument_id + h.level = instrument.proficiency_level + h.approve = true + $selectedInstruments.push(h) + }) + + renderDesiredExperienceLabel($selectedInstruments) + }); } @@ -593,11 +632,23 @@ invitationDialog.showFacebookDialog(); }); + $('a#choose-desired-experience').on("click", chooseExperience) + $('#band-setup').on('ifToggled', 'input[type="radio"].dependent-master', renderOptionalControls); $(friendInput).focus(function() { $(this).val(''); }) } + function chooseExperience(e) { + e.stopPropagation() + ui.launchInstrumentSelectorDialog("new member(s)", $selectedInstruments, function(selectedInstruments) { + $selectedInstruments = selectedInstruments + renderDesiredExperienceLabel($selectedInstruments) + return false + }) + return false + } + function initialize(invitationDialogInstance, friendSelectorDialog) { inviteMusiciansUtil = new JK.InviteMusiciansUtil(app); inviteMusiciansUtil.initialize(friendSelectorDialog); diff --git a/web/app/assets/javascripts/dialog/instrumentSelectorDialog.js b/web/app/assets/javascripts/dialog/instrumentSelectorDialog.js index b77c6fc1b..3346eef65 100644 --- a/web/app/assets/javascripts/dialog/instrumentSelectorDialog.js +++ b/web/app/assets/javascripts/dialog/instrumentSelectorDialog.js @@ -11,27 +11,33 @@ var $btnSelect = $screen.find(".btn-select-instruments"); var $instructions = $screen.find('.instructions'); var $instruments = $screen.find('.instruments'); - + var $instrumentSelectorContainer = $screen.find('.instrument-selector-container') + var instrumentSelector = new JK.InstrumentSelector(app, $instrumentSelectorContainer); + var $callback = callback + var selectedInstruments = instruments function beforeShow(data) { + instrumentSelector.initialize(false) + instrumentSelector.render($instrumentSelectorContainer) + instrumentSelector.setSelectedInstruments(selectedInstruments) } function afterShow(data) { - var instrumentList = context.JK.instruments; + // var instrumentList = context.JK.instruments; - $instruments.empty(); + // $instruments.empty(); - if (instrumentList) { - $.each(instrumentList, function(index, val) { - $instruments.append('
  • '); - var checked = ''; - if (instruments && $.inArray(val.id, instruments) > -1) { - checked = 'checked'; - } + // if (instrumentList) { + // $.each(instrumentList, function(index, val) { + // $instruments.append('
  • '); + // var checked = ''; + // if (instruments && $.inArray(val.id, selectedInstruments) > -1) { + // checked = 'checked'; + // } - $instruments.append('' + val.description); - $instruments.append('
  • '); - }); - } + // $instruments.append('' + val.description); + // $instruments.append(''); + // }); + // } } function afterHide() { @@ -45,17 +51,10 @@ function events() { $btnSelect.unbind("click").bind("click", function(evt) { evt.preventDefault(); - var selectedInstruments = []; - $instruments.find('input[type=checkbox]:checked').each(function(index) { - selectedInstruments.push($(this).val()); - }); - - if (callback) { - callback(selectedInstruments); - } - - app.layout.closeDialog(dialogId); + selectedInstruments = instrumentSelector.getSelectedInstruments() + $callback(selectedInstruments) + app.layout.closeDialog(dialogId); return false; }); @@ -70,7 +69,7 @@ app.bindDialog(dialogId, dialogBindings); - $instructions.html('Select one or more instruments for ' + type + ':'); + $instructions.html('Select the instruments and expertise you need for ' + type + ':'); events(); } diff --git a/web/app/assets/javascripts/instrumentSelector.js b/web/app/assets/javascripts/instrumentSelector.js index 57d6d6e51..51e740b75 100644 --- a/web/app/assets/javascripts/instrumentSelector.js +++ b/web/app/assets/javascripts/instrumentSelector.js @@ -4,18 +4,19 @@ context.JK = context.JK || {}; context.JK.InstrumentSelectorDeferred = null; - context.JK.InstrumentSelector = (function(app) { + context.JK.InstrumentSelector = (function(app, parentSelector) { var logger = context.JK.logger; var rest = new context.JK.Rest(); var _instruments = []; // will be list of structs: [ {label:xxx, value:yyy}, {...}, ... ] var _rsvp = false; - var _parentSelector = null; + if (typeof(_parentSelector)=="undefined") {_parentSelector=null} + var _parentSelector = parentSelector; var deferredInstruments = null; var self = this; function reset() { - $('input[type=checkbox]', _parentSelector).attr('checked', ''); + $('input[type="checkbox"]', _parentSelector).attr('checked', ''); if (_rsvp) { $('select.rsvp_count option', _parentSelector).eq(0).prop('selected', true); $('select.rsvp_level option', _parentSelector).eq(0).prop('selected', true); @@ -88,7 +89,7 @@ var $selectedVal = $('input[type="checkbox"]:checked', _parentSelector); $.each($selectedVal, function (index, value) { var id = $(value).attr('session-instrument-id'); - var name = $('label[for="' + $(value).attr('id') + '"]', _parentSelector).text(); + var name = $('label[for="' + $(value).attr('id') + '"]', _parentSelector).text().trim(); if (_rsvp) { var count = $('select[session-instrument-id="' + id + '"].rsvp-count', _parentSelector).val(); var rsvp_level = $('select[session-instrument-id="' + id + '"].rsvp-level', _parentSelector).val(); @@ -99,16 +100,16 @@ selectedInstruments.push({id: id, name: name, level: level}); } }); + return selectedInstruments; } function setSelectedInstruments(instrumentList) { if (!instrumentList) { return; - } - - $.each(instrumentList, function (index, value) { - $('input[type=checkbox][id="' + value.id + '"]') + } + $.each(instrumentList, function (index, value) { + $('input[type="checkbox"][session-instrument-id="' + value.id + '"]') .attr('checked', 'checked') .iCheck({ checkboxClass: 'icheckbox_minimal', @@ -116,11 +117,11 @@ inheritClass: true }); if (_rsvp) { - $('select[session-instrument-id="' + value.value + '"].rsvp-count', _parentSelector).val(value.count); - $('select[session-instrument-id="' + value.value + '"].rsvp-level', _parentSelector).val(value.level); + $('select[session-instrument-id="' + value.id + '"].rsvp-count', _parentSelector).val(value.count); + $('select[session-instrument-id="' + value.id + '"].rsvp-level', _parentSelector).val(value.level); } else { - $('select[session-instrument-id="' + value.value + '"]').val(value.level); + $('select[session-instrument-id="' + value.id + '"]').val(value.level); } }); } diff --git a/web/app/assets/javascripts/ui_helper.js b/web/app/assets/javascripts/ui_helper.js index c68ed7c09..11fb08af1 100644 --- a/web/app/assets/javascripts/ui_helper.js +++ b/web/app/assets/javascripts/ui_helper.js @@ -89,6 +89,7 @@ this.launchRsvpCreateSlotDialog = launchRsvpCreateSlotDialog; this.launchSessionStartDialog = launchSessionStartDialog; this.launchGenreSelectorDialog = launchGenreSelectorDialog; + this.launchInstrumentSelectorDialog = launchInstrumentSelectorDialog; this.launchRecordingSelectorDialog = launchRecordingSelectorDialog; return this; diff --git a/web/app/assets/stylesheets/dialogs/instrumentSelectorDialog.css.scss b/web/app/assets/stylesheets/dialogs/instrumentSelectorDialog.css.scss new file mode 100644 index 000000000..59760670d --- /dev/null +++ b/web/app/assets/stylesheets/dialogs/instrumentSelectorDialog.css.scss @@ -0,0 +1,44 @@ +@import "client/common"; + +#instrument-selector-dialog { + + min-height:initial; + + + .dialog-inner { + .content-body { + max-height: auto; + .content-body-scroller { + height: 350px; + overflow: scroll; + background-color:#c5c5c5; + border: 1px inset; + } + border: 1px solid #222; + margin: 4px 4px 8px 4px; + } + + .instructions { + font-size:16px; + } + + .dropdown { + box-shadow:0 0 0 0; + } + } + + .action-buttons { + margin-bottom:10px; + } + + .instruments { + + } + + label { + display:inline; + color: black; + font-size:16px; + } + +} \ No newline at end of file diff --git a/web/app/views/api_bands/show.rabl b/web/app/views/api_bands/show.rabl index d398dbbd3..dde9f8760 100644 --- a/web/app/views/api_bands/show.rabl +++ b/web/app/views/api_bands/show.rabl @@ -18,6 +18,10 @@ child :users => :musicians do end end +child :instruments => :instruments do + attributes :id, :instrument_id, :proficiency_level +end + child :genres => :genres do attributes :id, :description #partial('api_genres/index', :object => @band.genres) diff --git a/web/app/views/clients/_band_setup.html.slim b/web/app/views/clients/_band_setup.html.slim index 694eec75e..4a9e2c3f2 100644 --- a/web/app/views/clients/_band_setup.html.slim +++ b/web/app/views/clients/_band_setup.html.slim @@ -109,7 +109,7 @@ td.new-member-dependent label for="desired-experience" | Desired Experience   - a.select-desired-experience select + a#choose-desired-experience select td.new-member-dependent label for="play-commitment" Play Commitment td.new-member-dependent diff --git a/web/app/views/dialogs/_instrumentSelectorDialog.html.haml b/web/app/views/dialogs/_instrumentSelectorDialog.html.haml index ac6e44c2d..85ea6f938 100644 --- a/web/app/views/dialogs/_instrumentSelectorDialog.html.haml +++ b/web/app/views/dialogs/_instrumentSelectorDialog.html.haml @@ -2,14 +2,15 @@ .content-head = image_tag "content/icon_checkmark_circle.png", {:width => 20, :height => 20, :class => 'content-icon' } %h1 - = 'select instrument' + = 'select instruments & expertise' .dialog-inner %strong .instructions %br{:clear => "all"}/ .content-body .content-body-scroller - %ul.instruments.three-column-list-container + .instrument-selector-container + =render "users/instrument_selector" .right.action-buttons %a.button-grey.btn-cancel-dialog{'layout-action' => 'cancel'} CANCEL diff --git a/web/app/views/users/_instrument_selector.html.erb b/web/app/views/users/_instrument_selector.html.erb index 0b8c1d180..3101f6473 100644 --- a/web/app/views/users/_instrument_selector.html.erb +++ b/web/app/views/users/_instrument_selector.html.erb @@ -2,7 +2,7 @@ <% Instrument.standard_list.each do |instrument| %> <%= instrument.description %> - + <% end %> \ No newline at end of file From 7f02ca52408d414bc256ef1e1bfb5f38637398f0 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Sun, 31 May 2015 09:53:28 -0500 Subject: [PATCH 165/195] VRFS-3245 : Hook up help hover bubbles * Refactor common profile help bubble initialize convenience function to profile_utils. * Add new hover bubble text and hook up via HTML attribute on anchor tag --- .../javascripts/accounts_profile_interests.js | 12 ++------ web/app/assets/javascripts/band_setup.js | 29 ++++++++++--------- web/app/assets/javascripts/profile_utils.js | 8 +++++ web/app/views/clients/_band_setup.html.slim | 6 ++-- web/app/views/clients/_help.html.slim | 8 +++++ 5 files changed, 37 insertions(+), 26 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile_interests.js b/web/app/assets/javascripts/accounts_profile_interests.js index 4d44c835c..6257e69ed 100644 --- a/web/app/assets/javascripts/accounts_profile_interests.js +++ b/web/app/assets/javascripts/accounts_profile_interests.js @@ -286,17 +286,9 @@ inheritClass: true }) - initializeHelpBubbles() + profileUtils.initializeHelpBubbles($screen) } - - function initializeHelpBubbles() { - // Help bubbles (topics stored as attributes on element): - $(".help").each(function( index ) { - context.JK.helpBubble($(this), $(this).attr("help-topic"), {}, {}) - }) - } - - + this.initialize = initialize this.beforeShow = beforeShow this.afterShow = afterShow diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 15a869dc1..9f2ca3162 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -12,6 +12,7 @@ var NONE_SPECIFIED = 'None specified' var ui = new context.JK.UIHelper(JK.app) var logger = context.JK.logger; + var profileUtils = context.JK.ProfileUtils; var rest = context.JK.Rest(); var inviteMusiciansUtil = null; var invitationDialog = null; @@ -29,8 +30,9 @@ var concertCount=null; var currentStep = 0; var STEPS_COUNT=5; + var $screen=$("#band-setup") var $selectedInstruments=[] - + function navBack() { if (currentStep>0) { saveBand(function() { @@ -61,7 +63,7 @@ } function renderCurrentPage() { - $(".band-step").addClass("hidden") + $screen.find($(".band-step")).addClass("hidden") $("#band-setup-step-" + currentStep).removeClass("hidden") if(currentStep==0) { $("#btn-band-setup-back").addClass("hidden") @@ -80,17 +82,17 @@ if(e){e.stopPropagation()} // Is new member selected? - if ($('input[name="add_new_members"]:checked').val()=="yes") { - $(".new-member-dependent").removeClass("hidden") + if ($screen.find($('input[name="add_new_members"]:checked')).val()=="yes") { + $screen.find($(".new-member-dependent")).removeClass("hidden") } else { - $(".new-member-dependent").addClass("hidden") + $screen.find($(".new-member-dependent")).addClass("hidden") } // Is paid gigs selected? if ($('input[name="paid_gigs"]:checked').val()=="yes") { - $(".paid-gigs-dependent").removeClass("hidden") + $screen.find($(".paid-gigs-dependent")).removeClass("hidden") } else { - $(".paid-gigs-dependent").addClass("hidden") + $screen.find($(".paid-gigs-dependent")).addClass("hidden") } return false; } @@ -667,12 +669,13 @@ app.bindScreen('band/setup', screenBindings); - $('input[type=radio]') - .iCheck({ - checkboxClass: 'icheckbox_minimal', - radioClass: 'iradio_minimal', - inheritClass: true - }); + $('input[type=radio]').iCheck({ + checkboxClass: 'icheckbox_minimal', + radioClass: 'iradio_minimal', + inheritClass: true + }); + + profileUtils.initializeHelpBubbles() } this.initialize = initialize; diff --git a/web/app/assets/javascripts/profile_utils.js b/web/app/assets/javascripts/profile_utils.js index ee61b4272..d3c47f682 100644 --- a/web/app/assets/javascripts/profile_utils.js +++ b/web/app/assets/javascripts/profile_utils.js @@ -43,6 +43,7 @@ }; profileUtils.gigMap = { + "": "not specified", "0": "zero", "1": "under 10", "2": "10 to 50", @@ -83,6 +84,13 @@ return list; } + // Initialize standard profile help bubbles (topics stored as attributes on element): + profileUtils.initializeHelpBubbles = function(parentElement) { + $(".help", parentElement).each(function( index ) { + context.JK.helpBubble($(this), $(this).attr("help-topic"), {}, {}) + }) + } + // profile genres profileUtils.profileGenres = function(genres) { var matches = $.grep(genres, function(g) { diff --git a/web/app/views/clients/_band_setup.html.slim b/web/app/views/clients/_band_setup.html.slim index 4a9e2c3f2..4a21c6685 100644 --- a/web/app/views/clients/_band_setup.html.slim +++ b/web/app/views/clients/_band_setup.html.slim @@ -105,7 +105,7 @@ td label.strong-label for="new-member" | We want to add a new member   - a.help help-topic="profile-interests-virtual-band" [?] + a.help help-topic="band-profile-add-new-member" [?] td.new-member-dependent label for="desired-experience" | Desired Experience   @@ -138,7 +138,7 @@ td label.strong-label for="paid-gigs" | We want to play paid gigs   - a.help help-topic="profile-interests-paid-gigs" [?] + a.help help-topic="band-profile-play-paid-gigs" [?] td.paid-gigs-dependent label for="hourly-rate" Hourly Rate: td.paid-gigs-dependent @@ -159,7 +159,7 @@ td label.strong-label for="free-gigs" | We want to play free gigs   - a.help help-topic="profile-interests-free-gigs" [?] + a.help help-topic="band-profile-play-free-gigs" [?] tr td .radio-field diff --git a/web/app/views/clients/_help.html.slim b/web/app/views/clients/_help.html.slim index 390d3a2ca..dac0816e4 100644 --- a/web/app/views/clients/_help.html.slim +++ b/web/app/views/clients/_help.html.slim @@ -278,3 +278,11 @@ script type="text/template" id="template-help-profile-interests-free-sessions" script type="text/template" id="template-help-profile-interests-cowrite-partners" | For composers and songwriters who want to collaborate with others in composing and creating new music. +script type="text/template" id="template-help-band-profile-add-new-member" + | For bands that want to add one or more new members and let the JamKazam community of musicians know your band is looking for new members. + +script type="text/template" id="template-help-band-profile-play-paid-gigs" + | For professional bands that are qualified and interested in playing paid gigs at music venues and other events. + +script type="text/template" id="template-help-band-profile-play-free-gigs" + | For bands that are interested in playing free gigs, either for fun or to build experience. From a22a77bbb60bde2ce4d27ec0b1ab0136249d03aa Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Sun, 31 May 2015 14:54:29 -0500 Subject: [PATCH 166/195] VRFS-3245 : Fix genre validation on wrong step. --- web/app/assets/javascripts/band_setup.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 9f2ca3162..6a75cdfb7 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -10,6 +10,7 @@ context.JK.BandSetupScreen = function (app) { var NONE_SPECIFIED = 'None specified' + var GENRE_STEP = 1 var ui = new context.JK.UIHelper(JK.app) var logger = context.JK.logger; var profileUtils = context.JK.ProfileUtils; @@ -216,7 +217,7 @@ band.hourly_rate=$("#hourly-rate").val() band.gig_minimum=$("#gig-minimum").val() - if (currentStep==3) { + if (currentStep==GENRE_STEP) { band.genres = getSelectedGenres(); band.validate_genres = true } else { From a571f726eb435696d47023180e995739f6e0cd12 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Sun, 31 May 2015 16:05:41 -0500 Subject: [PATCH 167/195] VRFS-3245 : Correct parent so validation message displays. --- web/app/views/clients/_band_setup.html.slim | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/web/app/views/clients/_band_setup.html.slim b/web/app/views/clients/_band_setup.html.slim index 4a21c6685..f162ca4b5 100644 --- a/web/app/views/clients/_band_setup.html.slim +++ b/web/app/views/clients/_band_setup.html.slim @@ -65,9 +65,10 @@ table.band-form-table tr td#tdBandGenres rowspan="3" - label for="band-genres" What genres do you play? - .band-setup-genres - table#band-genres + .field + label for="band-genres" What genres do you play? + .band-setup-genres + table#band-genres td .field From 4dbf2e1c95ae587d68c1f50fc8b91fc74fd00c2c Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Sun, 31 May 2015 17:35:09 -0500 Subject: [PATCH 168/195] VRFS-3246 : Refactoring to allow online presence and samples controls to be used on bands -- incremental. --- ruby/jt_metadata.json | 2 +- .../javascripts/accounts_profile_samples.js | 129 +++++++++- web/app/assets/javascripts/band_setup.js | 38 +-- .../client/accountProfileSamples.css.scss | 6 +- .../assets/stylesheets/client/band.css.scss | 22 +- .../clients/_account_profile_samples.html.erb | 222 +----------------- web/app/views/clients/_band_setup.html.slim | 4 +- .../_profile_online_sample_controls.html.slim | 69 ++++++ .../stylesheets/icheck/minimal/minimal.css | 1 + 9 files changed, 242 insertions(+), 251 deletions(-) create mode 100644 web/app/views/clients/_profile_online_sample_controls.html.slim diff --git a/ruby/jt_metadata.json b/ruby/jt_metadata.json index bdadc014e..cc85875b4 100644 --- a/ruby/jt_metadata.json +++ b/ruby/jt_metadata.json @@ -1 +1 @@ -{"container_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150511-14158-1rprqit/jam-track-58.jkz", "version": "0", "coverart": null, "rsa_priv_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150511-14158-1rprqit/skey.pem", "tracks": [{"name": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150511-14158-1rprqit/27f81b9d-341e-4082-8a1b-662bf8f20a0d.ogg", "trackName": "track_00"}], "rsa_pub_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150511-14158-1rprqit/pkey.pem", "jamktrack_info": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/tmp2xkPZO"} \ No newline at end of file +{"container_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/jam-track-35.jkz", "version": "0", "coverart": null, "rsa_priv_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/skey.pem", "tracks": [{"name": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/7452fa4a-0c55-4cb2-948e-221475d7299c.ogg", "trackName": "track_00"}], "rsa_pub_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/pkey.pem", "jamktrack_info": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/tmpGdncJS"} \ No newline at end of file diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 52b27d13a..58a3bc7d2 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -3,7 +3,7 @@ "use strict"; context.JK = context.JK || {}; - context.JK.AccountProfileSamples = function(app) { + context.JK.AccountProfileSamples = function(app, parent) { var $document = $(document); // used to initialize RecordingSourceValidator in site_validator.js.coffee @@ -15,10 +15,10 @@ var EVENTS = context.JK.EVENTS; var api = context.JK.Rest(); var ui = new context.JK.UIHelper(JK.app); - var user = {}; + var target = {}; var profileUtils = context.JK.ProfileUtils; - var $screen = $('#account-profile-samples'); + var $screen = $('.account-profile-samples', parent); // online presences var $website = $screen.find('#website'); @@ -315,6 +315,126 @@ } } + function removeRow(recordingId, type) { + $('div[data-recording-id=' + recordingId + ']').remove(); + + if (type === 'soundcloud') { + window.soundCloudRecordingValidator.removeRecordingId(recordingId); + } + else if (type === 'youtube') { + window.youTubeRecordingValidator.removeRecordingId(recordingId); + } + } + + function formatTitle(title) { + return title && title.length > 30 ? title.substring(0, 30) + "..." : title; + } + + // This function is a bit of a mess. It was pulled + // from the html.erb file verbatim, and could use a + // refactor: + function initializeValidators() { + var initialized = false; + $(document).on('INIT_SITE_VALIDATORS', function(e, data) { + window.soundCloudRecordingValidator.init(window.soundCloudRecordingSources); + window.youTubeRecordingValidator.init(window.youTubeRecordingSources); + }); + + $(document).on('JAMKAZAM_READY', function(e, data) { + window.JK.JamServer.get$Server().on(window.JK.EVENTS.CONNECTION_UP, function() { + if(initialized) { + return; + } + initialized = true; + + //var $screen = $('#account-profile-samples'); + var $btnAddSoundCloudRecording = $screen.find('#btn-add-soundcloud-recording'); + var $btnAddYouTubeVideo = $screen.find('#btn-add-youtube-video'); + var $soundCloudSampleList = $screen.find('.samples.soundcloud'); + var $youTubeSampleList = $screen.find('.samples.youtube'); + + setTimeout(function() { + window.urlValidator = new JK.SiteValidator('url', userNameSuccessCallback, userNameFailCallback); + window.urlValidator.init(); + + window.soundCloudValidator = new JK.SiteValidator('soundcloud', userNameSuccessCallback, userNameFailCallback); + window.soundCloudValidator.init(); + + window.reverbNationValidator = new JK.SiteValidator('reverbnation', userNameSuccessCallback, userNameFailCallback); + window.reverbNationValidator.init(); + + window.bandCampValidator = new JK.SiteValidator('bandcamp', userNameSuccessCallback, userNameFailCallback); + window.bandCampValidator.init(); + + window.fandalismValidator = new JK.SiteValidator('fandalism', userNameSuccessCallback, userNameFailCallback); + window.fandalismValidator.init(); + + window.youTubeValidator = new JK.SiteValidator('youtube', userNameSuccessCallback, userNameFailCallback); + window.youTubeValidator.init(); + + window.facebookValidator = new JK.SiteValidator('facebook', userNameSuccessCallback, userNameFailCallback); + window.facebookValidator.init(); + + window.twitterValidator = new JK.SiteValidator('twitter', userNameSuccessCallback, userNameFailCallback); + window.twitterValidator.init(); + + window.soundCloudRecordingValidator = new JK.RecordingSourceValidator('rec_soundcloud', soundCloudSuccessCallback, siteFailCallback); + + window.youTubeRecordingValidator = new JK.RecordingSourceValidator('rec_youtube', youTubeSuccessCallback, siteFailCallback); + }, 1); + + + function userNameSuccessCallback($inputDiv) { + $inputDiv.removeClass('error'); + $inputDiv.find('.error-text').remove(); + } + + function userNameFailCallback($inputDiv) { + $inputDiv.addClass('error'); + $inputDiv.find('.error-text').remove(); + $inputDiv.append("Invalid username").show(); + } + + function soundCloudSuccessCallback($inputDiv) { + siteSuccessCallback($inputDiv, window.soundCloudRecordingValidator, $soundCloudSampleList, 'soundcloud'); + } + + function youTubeSuccessCallback($inputDiv) { + siteSuccessCallback($inputDiv, window.youTubeRecordingValidator, $youTubeSampleList, 'youtube'); + } + + function siteSuccessCallback($inputDiv, recordingSiteValidator, $sampleList, type) { + $inputDiv.removeClass('error'); + $inputDiv.find('.error-text').remove(); + + var recordingSources = recordingSiteValidator.recordingSources(); + if (recordingSources && recordingSources.length > 0) { + var $sampleList = $sampleList.find('.sample-list'); + var addedRecording = recordingSources[recordingSources.length-1]; + + // TODO: this code is repeated in JS file + var recordingIdAttr = ' data-recording-id="' + addedRecording.recording_id + '" '; + var recordingUrlAttr = ' data-recording-url="' + addedRecording.url + '" '; + var recordingTitleAttr = ' data-recording-title="' + addedRecording.recording_title + '"'; + var title = formatTitle(addedRecording.recording_title); + $sampleList.append('
    ' + title + '
    '); + + var onclick = "onclick=removeRow(\'" + addedRecording.recording_id + "\',\'" + type + "\');"; + $sampleList.append('
    X
    '); + } + + $inputDiv.find('input').val(''); + } + + function siteFailCallback($inputDiv) { + $inputDiv.addClass('error'); + $inputDiv.find('.error-text').remove(); + $inputDiv.append("Invalid URL").show(); + } + }); + }); + } // end initializeValidators. + function initialize() { var screenBindings = { 'beforeShow': beforeShow, @@ -322,7 +442,7 @@ }; app.bindScreen('account/profile/samples', screenBindings); - + initializeValidators(); events(); } @@ -331,5 +451,4 @@ this.afterShow = afterShow; return this; }; - })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 6a75cdfb7..9c87aca44 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -32,7 +32,9 @@ var currentStep = 0; var STEPS_COUNT=5; var $screen=$("#band-setup") + var $samples = $screen.find(".account-profile-samples") var $selectedInstruments=[] + var accountProfileSamples = null; function navBack() { if (currentStep>0) { @@ -177,21 +179,22 @@ } function renderErrors(errors) { + logger.debug("Band setup errors: ", errors) var name = context.JK.format_errors("name", errors); var country = context.JK.format_errors("country", errors); var state = context.JK.format_errors("state", errors); var city = context.JK.format_errors("city", errors); var biography = context.JK.format_errors("biography", errors); - var genres = context.JK.format_errors("genres", errors); var website = context.JK.format_errors("website", errors); + var genres = context.JK.format_errors("genres", errors); - if(name) $("#band-name").closest('div.field').addClass('error').end().after(name); - if(country) $("#band-country").closest('div.field').addClass('error').end().after(country); - if(state) $("#band-region").closest('div.field').addClass('error').end().after(state); - if(city) $("#band-city").closest('div.field').addClass('error').end().after(city); - if(genres) $(".band-setup-genres").closest('div.field').addClass('error').end().after(genres); + if(name) $("#band-name").closest('div.field').addClass('error').end().after(name); + if(country) $("#band-country").closest('div.field').addClass('error').end().after(country); + if(state) $("#band-region").closest('div.field').addClass('error').end().after(state); + if(city) $("#band-city").closest('div.field').addClass('error').end().after(city); if(biography) $("#band-biography").closest('div.field').addClass('error').end().after(biography); - if(website) $("#band-website").closest('div.field').addClass('error').end().after(website); + if(website) $("#band-website").closest('div.field').addClass('error').end().after(website); + if(genres) $("#band-genres").closest('div.field').addClass('error').end().after(genres); } function buildBand() { @@ -223,7 +226,6 @@ } else { band.validate_genres = false } - $.each($selectedInstruments, function(index, instrument) { var h = {} @@ -653,28 +655,32 @@ } function initialize(invitationDialogInstance, friendSelectorDialog) { - inviteMusiciansUtil = new JK.InviteMusiciansUtil(app); - inviteMusiciansUtil.initialize(friendSelectorDialog); - friendInput = inviteMusiciansUtil.inviteBandCreate('#band-setup-invite-musicians', "
    If your bandmates are already on JamKazam, start typing their names in the box below, or click the Choose Friends button to select them.
    "); - invitationDialog = invitationDialogInstance; - events(); + inviteMusiciansUtil = new JK.InviteMusiciansUtil(app) + inviteMusiciansUtil.initialize(friendSelectorDialog) + + accountProfileSamples = new JK.AccountProfileSamples(JK.app, $screen) + accountProfileSamples.initialize() + + friendInput = inviteMusiciansUtil.inviteBandCreate('#band-setup-invite-musicians', "
    If your bandmates are already on JamKazam, start typing their names in the box below, or click the Choose Friends button to select them.
    ") + invitationDialog = invitationDialogInstance + events() var screenBindings = { 'beforeShow': beforeShow, 'afterShow': afterShow - }; + } bandType=$("#band-type") bandStatus=$("#band-status") concertCount=$("#concert-count") - app.bindScreen('band/setup', screenBindings); + app.bindScreen('band/setup', screenBindings) $('input[type=radio]').iCheck({ checkboxClass: 'icheckbox_minimal', radioClass: 'iradio_minimal', inheritClass: true - }); + }) profileUtils.initializeHelpBubbles() } diff --git a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss index 6fa2a5372..f2c47d585 100644 --- a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss +++ b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss @@ -1,8 +1,12 @@ @import "common.css.scss"; @import "site_validator.css.scss"; -#account-profile-samples { +.profile-online-sample-controls { + .sample-row { + position: relative; + } + .sample { font-weight: 600; font-size: 16px; diff --git a/web/app/assets/stylesheets/client/band.css.scss b/web/app/assets/stylesheets/client/band.css.scss index 7d76277b3..8cc2c147a 100644 --- a/web/app/assets/stylesheets/client/band.css.scss +++ b/web/app/assets/stylesheets/client/band.css.scss @@ -7,10 +7,14 @@ @include border_box_sizing; width: 100%; padding: 2px 4px 2px 2px; + overflow: hidden; + } + + textarea { + overflow:hidden; } } - // Mimic style of easydropdown selects: input[type="number"] { border-radius: 6px; @@ -21,6 +25,10 @@ padding: 5px; } + .content-body-scroller { + + } + .radio-field { display: inline; padding: 2px; @@ -57,7 +65,7 @@ box-shadow: inset 2px 2px 3px 0px #888; color:#000; overflow:auto; - font-size:14px; + font-size:12px; } .band-setup-photo { @@ -106,7 +114,7 @@ font-weight:200; font-size: 28px; float:left; - margin: 0px 150px 0px 0px; + margin: 0 0 1em 0; } .band-profile-status { @@ -141,7 +149,7 @@ padding-top:65px; background-color:#535353; color:#ccc; - font-size:17px; + font-size:16px; text-decoration:none; } @@ -380,7 +388,7 @@ #band-setup-form { - padding: 0.25em 0.5em 1.25em 0.25em; + margin: 0.25em 0.5em 1.25em 0.25em; table.band-form-table { width: 100%; margin: 1em; @@ -421,12 +429,12 @@ } label { - font-size: 1.1em; + font-size: 1.05em; } label.strong-label { font-weight: bold; - font-size: 1.2em; + font-size: 1.1em; } } \ No newline at end of file diff --git a/web/app/views/clients/_account_profile_samples.html.erb b/web/app/views/clients/_account_profile_samples.html.erb index 74943cf2e..df5392e0f 100644 --- a/web/app/views/clients/_account_profile_samples.html.erb +++ b/web/app/views/clients/_account_profile_samples.html.erb @@ -1,4 +1,4 @@ -
    +
    - - - diff --git a/web/app/views/clients/_band_setup.html.slim b/web/app/views/clients/_band_setup.html.slim index f162ca4b5..5fe8d6660 100644 --- a/web/app/views/clients/_band_setup.html.slim +++ b/web/app/views/clients/_band_setup.html.slim @@ -172,7 +172,8 @@ #band-setup-step-3.band-step.content-wrapper h2 set up band: online presence & performance samples - table.band-form-table + = render "clients/profile_online_sample_controls" + #band-setup-step-4.band-step.content-wrapper h2 invite members br @@ -218,6 +219,7 @@ a#btn-band-setup-next.nav-button.button-orange | SAVE & NEXT .clearall + br clear="all" script#template-band-setup-genres type="text/template" diff --git a/web/app/views/clients/_profile_online_sample_controls.html.slim b/web/app/views/clients/_profile_online_sample_controls.html.slim new file mode 100644 index 000000000..604804288 --- /dev/null +++ b/web/app/views/clients/_profile_online_sample_controls.html.slim @@ -0,0 +1,69 @@ +.profile-online-sample-controls + .sample_row + .left + label Website (URL): + #url_validator.left.presence.site_validator + input#website maxlength="4000" type="text" + span.spinner-small + .left + label SoundCloud (username): + #soundcloud_validator.left.presence.site_validator + input#soundcloud-username maxlength="100" type="text" + span.spinner-small + .left + label ReverbNation (username): + #reverbnation_validator.left.presence.site_validator + input#reverbnation-username maxlength="100" type="text" + span.spinner-small + .left + label Bandcamp (username): + #bandcamp_validator.left.presence.site_validator + input#bandcamp-username maxlength="100" type="text" + span.spinner-small + .sample_row + .left + label Fandalism (URL): + #fandalism_validator.left.presence.site_validator + input#fandalism-username maxlength="100" type="text" + span.spinner-small + .left + label YouTube (username): + #youtube_validator.left.presence.site_validator + input#youtube-username maxlength="100" type="text" + span.spinner-small + .left + label Facebook (username): + #facebook_validator.left.presence.site_validator + input#facebook-username maxlength="100" type="text" + span.spinner-small + .left + label Twitter (username): + #twitter_validator.left.presence.site_validator + input#twitter-username maxlength="100" type="text" + span.spinner-small + .sample_row.samples-container + .left.samples.jamkazam + label JamKazam Recordings: + .controls + .left + a#btn-add-jk-recording.button-grey BROWSE + br.clearall + .sample-list + .left.samples.soundcloud + label SoundCloud Recordings (URL): + .controls + #rec_soundcloud_validator.left.sample.site_validator + input#soundcloud-recording maxlength="4000" type="text" + a#btn-add-soundcloud-recording.button-grey.add-recording-source ADD + span.spinner-small + br.clearall + .sample-list + .left.samples.youtube + label YouTube Videos (URL): + .controls + #rec_youtube_validator.left.sample.site_validator + input#youtube-video maxlength="4000" type="text" + a#btn-add-youtube-video.button-grey.add-recording-source ADD + span.spinner-small + br.clearall + .sample-list \ No newline at end of file diff --git a/web/vendor/assets/stylesheets/icheck/minimal/minimal.css b/web/vendor/assets/stylesheets/icheck/minimal/minimal.css index fea9c54f1..ffda1e558 100644 --- a/web/vendor/assets/stylesheets/icheck/minimal/minimal.css +++ b/web/vendor/assets/stylesheets/icheck/minimal/minimal.css @@ -12,6 +12,7 @@ cursor: pointer; } + .icheckbox_minimal { background-position: 0 0; } From 1f076dcc75ddcdc43094186f4e171d1b818853de Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Sun, 31 May 2015 20:14:18 -0500 Subject: [PATCH 169/195] VRFS-3246 : More reliable layout and styling for online presence and performance samples. --- .../javascripts/accounts_profile_samples.js | 20 ++- .../client/accountProfileSamples.css.scss | 70 +++----- .../_profile_online_sample_controls.html.slim | 155 ++++++++++-------- 3 files changed, 121 insertions(+), 124 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 58a3bc7d2..88084449e 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -48,8 +48,7 @@ $.when(api.getUserProfile()) .done(function(userDetail) { renderPresence(userDetail); - renderSamples(userDetail); - $document.triggerHandler('INIT_SITE_VALIDATORS'); + renderSamples(userDetail); }); } @@ -335,11 +334,6 @@ // refactor: function initializeValidators() { var initialized = false; - $(document).on('INIT_SITE_VALIDATORS', function(e, data) { - window.soundCloudRecordingValidator.init(window.soundCloudRecordingSources); - window.youTubeRecordingValidator.init(window.youTubeRecordingSources); - }); - $(document).on('JAMKAZAM_READY', function(e, data) { window.JK.JamServer.get$Server().on(window.JK.EVENTS.CONNECTION_UP, function() { if(initialized) { @@ -379,8 +373,10 @@ window.twitterValidator.init(); window.soundCloudRecordingValidator = new JK.RecordingSourceValidator('rec_soundcloud', soundCloudSuccessCallback, siteFailCallback); - + logger.debug("window.soundCloudRecordingValidator", window.soundCloudRecordingValidator) window.youTubeRecordingValidator = new JK.RecordingSourceValidator('rec_youtube', youTubeSuccessCallback, siteFailCallback); + + $document.triggerHandler('INIT_SITE_VALIDATORS'); }, 1); @@ -433,6 +429,14 @@ } }); }); + + $(document).on('INIT_SITE_VALIDATORS', function(e, data) { + logger.debug("ZZZwindow.soundCloudRecordingValidator", window.soundCloudRecordingValidator) + window.soundCloudRecordingValidator.init(window.soundCloudRecordingSources); + window.youTubeRecordingValidator.init(window.youTubeRecordingSources); + }); + + } // end initializeValidators. function initialize() { diff --git a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss index f2c47d585..f256dada7 100644 --- a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss +++ b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss @@ -1,10 +1,26 @@ @import "common.css.scss"; @import "site_validator.css.scss"; -.profile-online-sample-controls { - +.profile-online-sample-controls { + table.profile-table { + width: 100%; + tr:nth-child(even) td { + padding: 0.25em 0.25em 1em 0.25em; + vertical-align: top; + } + tr:nth-child(odd) td { + padding: 0.25em; + vertical-align: top; + } + } + + table.control-table { + width: 100%; + } + .sample-row { position: relative; + clear: both; } .sample { @@ -13,54 +29,12 @@ } .presence { - margin: 3px 30px 15px 0px; - - input { - width:200px; - } + margin: 3px 30px 15px 0px; } - .samples-container { - overflow: hidden; - .samples { - width: 30%; - margin: 0.5em; - label { - height: 1.5em; - } - .controls { - height: 2em; - } - input { - width: 200px; - } - - .sample { - margin: 3px 5px 10px 0px; - - a.add-recording-source { - margin-top: 2px !important; - } - } - - div.sample-list { - min-height: 150px; - width: auto; - min-width: 100px; - border: 2px solid #ccc; - overflow: scroll; - } - - div.entry { - margin-left: 10px; - margin-top: 5px; - } - - div.close-button { - margin-right: 15px; - margin-top: 5px; - cursor: pointer; - } + .site_validator { + a, .spinner-small { + margin: -7px 1px 2px 2px; } } } \ No newline at end of file diff --git a/web/app/views/clients/_profile_online_sample_controls.html.slim b/web/app/views/clients/_profile_online_sample_controls.html.slim index 604804288..6c349835f 100644 --- a/web/app/views/clients/_profile_online_sample_controls.html.slim +++ b/web/app/views/clients/_profile_online_sample_controls.html.slim @@ -1,69 +1,88 @@ .profile-online-sample-controls - .sample_row - .left - label Website (URL): - #url_validator.left.presence.site_validator - input#website maxlength="4000" type="text" - span.spinner-small - .left - label SoundCloud (username): - #soundcloud_validator.left.presence.site_validator - input#soundcloud-username maxlength="100" type="text" - span.spinner-small - .left - label ReverbNation (username): - #reverbnation_validator.left.presence.site_validator - input#reverbnation-username maxlength="100" type="text" - span.spinner-small - .left - label Bandcamp (username): - #bandcamp_validator.left.presence.site_validator - input#bandcamp-username maxlength="100" type="text" - span.spinner-small - .sample_row - .left - label Fandalism (URL): - #fandalism_validator.left.presence.site_validator - input#fandalism-username maxlength="100" type="text" - span.spinner-small - .left - label YouTube (username): - #youtube_validator.left.presence.site_validator - input#youtube-username maxlength="100" type="text" - span.spinner-small - .left - label Facebook (username): - #facebook_validator.left.presence.site_validator - input#facebook-username maxlength="100" type="text" - span.spinner-small - .left - label Twitter (username): - #twitter_validator.left.presence.site_validator - input#twitter-username maxlength="100" type="text" - span.spinner-small - .sample_row.samples-container - .left.samples.jamkazam - label JamKazam Recordings: - .controls - .left - a#btn-add-jk-recording.button-grey BROWSE - br.clearall - .sample-list - .left.samples.soundcloud - label SoundCloud Recordings (URL): - .controls - #rec_soundcloud_validator.left.sample.site_validator - input#soundcloud-recording maxlength="4000" type="text" - a#btn-add-soundcloud-recording.button-grey.add-recording-source ADD - span.spinner-small - br.clearall - .sample-list - .left.samples.youtube - label YouTube Videos (URL): - .controls - #rec_youtube_validator.left.sample.site_validator - input#youtube-video maxlength="4000" type="text" - a#btn-add-youtube-video.button-grey.add-recording-source ADD - span.spinner-small - br.clearall - .sample-list \ No newline at end of file + table.profile-table + tr + td colspan="25%": label Website (URL): + td colspan="25%": label SoundCloud (username): + td colspan="25%": label ReverbNation (username): + td colspan="25%": label Bandcamp (username): + tr + td colspan="25%" + table.control-table: tr#url_validator.presence.site_validator + td colspan="100%" + input#website maxlength="4000" type="text" + td + span.spinner-small + td colspan="25%" + table.control-table: tr#soundcloud_validator.presence.site_validator + td colspan="100%" + input#soundcloud-username maxlength="100" type="text" + td + span.spinner-small + td colspan="25%" + table.control-table: tr#reverbnation_validator.presence.site_validator + td colspan="100%" + input#reverbnation-username maxlength="100" type="text" + td + span.spinner-small + td colspan="25%" + table.control-table: tr#bandcamp_validator.presence.site_validator + td colspan="100%" + input#bandcamp-username maxlength="100" type="text" + td + span.spinner-small + + tr + td colspan="25%": label Fandalism (URL): + td colspan="25%": label YouTube (username): + td colspan="25%": label Facebook (username): + td colspan="25%": label Twitter (username): + tr + td colspan="25%" + table.control-table: tr#fandalism_validator.presence.site_validator + td colspan="100%" + input#fandalism-username maxlength="100" type="text" + td + span.spinner-small + td colspan="25%" + table.control-table: tr#youtube_validator.presence.site_validator + td colspan="100%" + input#youtube-username maxlength="100" type="text" + td + span.spinner-small + td colspan="25%" + table.control-table: tr#facebook_validator.presence.site_validator + td colspan="100%" + input#facebook-username maxlength="100" type="text" + td + span.spinner-small + td colspan="25%" + table.control-table: tr#twitter_validator.presence.site_validator + td colspan="100%" + input#twitter-username maxlength="100" type="text" + td + span.spinner-small + tr + td colspan="33.33%": label JamKazam Recordings: + td colspan="33.33%": label SoundCloud Recordings (URL): + td colspan="33.33%": label YouTube Videos (URL): + tr + td colspan="33.33%" + a#btn-add-jk-recording.button-grey BROWSE + td colspan="33.33%" + table.control-table: tr#rec_soundcloud_validator.sample.site_validator + td colspan="100%" + input#soundcloud-recording maxlength="4000" type="text" + td + a#btn-add-soundcloud-recording.button-grey.add-recording-source ADD + span.spinner-small + td colspan="33.33%" + table.control-table: tr#rec_youtube_validator.sample.site_validator + td colspan="100%" + input#youtube-video maxlength="4000" type="text" + td + a#btn-add-youtube-video.button-grey.add-recording-source ADD + span.spinner-small + tr + td colspan="33.33%": .sample-list + td colspan="33.33%": .sample-list + td colspan="33.33%": .sample-list \ No newline at end of file From bd8287b5285eff0cbee808c818836a88199de785 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Sun, 31 May 2015 21:50:04 -0500 Subject: [PATCH 170/195] VRFS-3246 : Remove references to current_user, change to target_player. --- ruby/lib/jam_ruby/models/online_presence.rb | 19 ++++++++++--------- .../lib/jam_ruby/models/performance_sample.rb | 15 ++++++++------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/ruby/lib/jam_ruby/models/online_presence.rb b/ruby/lib/jam_ruby/models/online_presence.rb index 2e8e63bd5..6f97baa7f 100644 --- a/ruby/lib/jam_ruby/models/online_presence.rb +++ b/ruby/lib/jam_ruby/models/online_presence.rb @@ -21,12 +21,13 @@ module JamRuby OnlinePresence.where("player_id = ?", options[:id]) end - def self.create(player, options = {}, save = true) - auth_player(player, options) + # Create with target_player (target_player is either user or band): + def self.create(target_player, options = {}, save = true) + auth_player(target_player, options) raise StateError, "Missing required information" if options[:service_type].blank? || options[:username].blank? up = OnlinePresence.new({ - :player_id => player.id, + :player_id => target_player.id, :service_type => options[:service_type], :username => options[:username] }) @@ -35,8 +36,8 @@ module JamRuby up end - def self.update(player, options = {}) - auth_player(player, options) + def self.update(target_player, options = {}) + auth_player(target_player, options) raise StateError, "Missing required information" if options[:service_type].blank? || options[:username].blank? || options[:id].blank? up = OnlinePresence.find(options[:id]) @@ -45,12 +46,12 @@ module JamRuby up.save! end - def self.delete(player, options = {}) + def self.delete(target_player, options = {}) id = options[:id] raise StateError, "Missing required information" if id.blank? online_presence = OnlinePresence.find(id) - if online_presence.player_id != player.id + if online_presence.player_id != target_player.id raise JamPermissionError, PERMISSION_MSG end @@ -60,8 +61,8 @@ module JamRuby end private - def self.auth_player(player, options={}) - raise JamPermissionError, PERMISSION_MSG if player.nil? || options[:player_id] != player.id + def self.auth_player(target_player, options={}) + raise JamPermissionError, PERMISSION_MSG if target_player.nil? || options[:player_id] != target_player.id end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/performance_sample.rb b/ruby/lib/jam_ruby/models/performance_sample.rb index b009484e8..38ed618bb 100644 --- a/ruby/lib/jam_ruby/models/performance_sample.rb +++ b/ruby/lib/jam_ruby/models/performance_sample.rb @@ -41,12 +41,13 @@ module JamRuby PerformanceSample.where("player_id = ?", options[:id]) end - def self.create(current_user, options = {}, save = true) - auth_user(current_user, options) + # Create with target_player (target_player is either user or band): + def self.create(target_player, options = {}, save = true) + auth_user(target_player, options) raise StateError, "Missing required information" if options[:service_type].blank? ps = PerformanceSample.new({ - :player_id => current_user.id, + :player_id => target_player.id, :service_type => options[:service_type], :claimed_recording_id => options[:claimed_recording_id], :service_id => options[:service_id], @@ -58,15 +59,15 @@ module JamRuby ps end - def self.delete(current_user, options = {}) - raise JamPermissionError, "You do not have permission to perform this operation" if current_user.nil? || options[:player_id] != current_user.id + def self.delete(target_player, options = {}) + raise JamPermissionError, "You do not have permission to perform this operation" if target_player.nil? || options[:player_id] != target_player.id raise StateError, "The performance sample ID is missing." if options[:id].blank? PerformanceSample.destroy(options[:id]) end private - def self.auth_user(current_user, options={}) - raise JamPermissionError, PERMISSION_MSG if current_user.nil? || options[:player_id] != current_user.id + def self.auth_user(target_player, options={}) + raise JamPermissionError, PERMISSION_MSG if target_player.nil? || options[:player_id] != target_player.id end end end \ No newline at end of file From d60eff50ba2692a4e9c34b02c2103247e970a180 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Tue, 2 Jun 2015 15:13:41 -0500 Subject: [PATCH 171/195] VRFS-3246 : Support online presences and performance samples through sub-widget. Incremental. --- ruby/lib/jam_ruby/models/band.rb | 26 +++++- .../javascripts/accounts_profile_samples.js | 87 ++++++++++++------- web/app/assets/javascripts/band_setup.js | 82 ++++++++++------- web/app/controllers/api_bands_controller.rb | 2 + web/app/views/clients/index.html.erb | 6 +- 5 files changed, 139 insertions(+), 64 deletions(-) diff --git a/ruby/lib/jam_ruby/models/band.rb b/ruby/lib/jam_ruby/models/band.rb index a0c3a5033..6d6c6f96e 100644 --- a/ruby/lib/jam_ruby/models/band.rb +++ b/ruby/lib/jam_ruby/models/band.rb @@ -179,7 +179,7 @@ module JamRuby band = id.blank? ? Band.new : Band.find(id) # ensure user updating Band details is a Band member - unless band.new_record? || band.users.exists?(user) + unless band.new_record? || band.users.exists?(user) raise JamPermissionError, ValidationMessages::USER_NOT_BAND_MEMBER_VALIDATION_ERROR end @@ -200,12 +200,34 @@ module JamRuby end band.skip_genre_validation = true unless params[:validate_genres] - puts "SKIPPING GENRE VALIDATION: #{band.skip_genre_validation}" + + unless band.new_record? + OnlinePresence.delete_all(["player_id = ?", band.id]) + PerformanceSample.delete_all(["player_id = ?", band.id]) + end + + online_presences = params[:online_presences] + Rails.logger.info("ONLINE_PRESENCE: #{online_presences.inspect} / #{params.inspect}") + if online_presences.present? + online_presences.each do |op| + new_presence = OnlinePresence.create(band, op, false) + band.online_presences << new_presence + end + end + + performance_samples = params[:performance_samples] + if performance_samples.present? + performance_samples.each do |ps| + band.performance_samples << PerformanceSample.create(band, ps, false) + end + end + band end # helper method for creating / updating a Band def self.save(user, params) + puts "Band.save with #{user}, #{params}" band = build_band(user, params) if band.save diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 88084449e..aebe9b51e 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -3,7 +3,10 @@ "use strict"; context.JK = context.JK || {}; - context.JK.AccountProfileSamples = function(app, parent) { + + // TODO: Add a target type, which can be band or user -- call the + // appropriate API methods. + context.JK.AccountProfileSamples = function(app, parent, loadFn, updateFn) { var $document = $(document); // used to initialize RecordingSourceValidator in site_validator.js.coffee @@ -17,11 +20,9 @@ var ui = new context.JK.UIHelper(JK.app); var target = {}; var profileUtils = context.JK.ProfileUtils; - - var $screen = $('.account-profile-samples', parent); - + var $screen = $('.profile-online-sample-controls', parent); // online presences - var $website = $screen.find('#website'); + var $website = $screen.find('#website'); var $soundCloudUsername = $screen.find('#soundcloud-username'); var $reverbNationUsername = $screen.find('#reverbnation-username'); var $bandCampUsername = $screen.find('#bandcamp-username'); @@ -44,71 +45,78 @@ function beforeShow(data) { } - function afterShow(data) { - $.when(api.getUserProfile()) - .done(function(userDetail) { - renderPresence(userDetail); - renderSamples(userDetail); + function afterShow(data) { + //$.when(api.getUserProfile()) + $.when(loadFn()) + .done(function(targetPlayer) { + if (targetPlayer && targetPlayer.keys && targetPlayer.keys.length > 0) { + renderPlayer(targetPlayer) + } }); } - function renderPresence(user) { - $website.val(user.website); + function renderPlayer(targetPlayer) { + renderPresence(targetPlayer); + renderSamples(targetPlayer); + } + + function renderPresence(targetPlayer) { + $website.val(targetPlayer.website); // SoundCloud - var presences = profileUtils.soundCloudPresences(user.online_presences); + var presences = profileUtils.soundCloudPresences(targetPlayer.online_presences); if (presences && presences.length > 0) { $soundCloudUsername.val(presences[0].username); } // ReverbNation - presences = profileUtils.reverbNationPresences(user.online_presences); + presences = profileUtils.reverbNationPresences(targetPlayer.online_presences); if (presences && presences.length > 0) { $reverbNationUsername.val(presences[0].username); } // Bandcamp - presences = profileUtils.bandCampPresences(user.online_presences); + presences = profileUtils.bandCampPresences(targetPlayer.online_presences); if (presences && presences.length > 0) { $bandCampUsername.val(presences[0].username); } // Fandalism - presences = profileUtils.fandalismPresences(user.online_presences); + presences = profileUtils.fandalismPresences(targetPlayer.online_presences); if (presences && presences.length > 0) { $fandalismUsername.val(presences[0].username); } // YouTube - presences = profileUtils.youTubePresences(user.online_presences); + presences = profileUtils.youTubePresences(targetPlayer.online_presences); if (presences && presences.length > 0) { $youTubeUsername.val(presences[0].username); } // Facebook - presences = profileUtils.facebookPresences(user.online_presences); + presences = profileUtils.facebookPresences(targetPlayer.online_presences); if (presences && presences.length > 0) { $facebookUsername.val(presences[0].username); } // Twitter - presences = profileUtils.twitterPresences(user.online_presences); + presences = profileUtils.twitterPresences(targetPlayer.online_presences); if (presences && presences.length > 0) { $twitterUsername.val(presences[0].username); } } - function renderSamples(user) { + function renderSamples(targetPlayer) { // JamKazam recordings - var samples = profileUtils.jamkazamSamples(user.performance_samples); + var samples = profileUtils.jamkazamSamples(targetPlayer.performance_samples); loadSamples(samples, 'jamkazam', $jamkazamSampleList, window.jamkazamRecordingSources); // SoundCloud recordings - samples = profileUtils.soundCloudSamples(user.performance_samples); + samples = profileUtils.soundCloudSamples(targetPlayer.performance_samples); loadSamples(samples, 'soundcloud', $soundCloudSampleList, window.soundCloudRecordingSources); // YouTube videos - samples = profileUtils.youTubeSamples(user.performance_samples); + samples = profileUtils.youTubeSamples(targetPlayer.performance_samples); loadSamples(samples, 'youtube', $youTubeSampleList, window.youTubeRecordingSources); } @@ -269,9 +277,22 @@ function handleUpdateProfile() { disableSubmits() + + var player = buildPlayer() + updateFn({ + website: player.website, + online_presences: player.online_presences, + performance_samples: player.performance_samples + }) + .done(postUpdateProfileSuccess) + .fail(postUpdateProfileFailure) + .always(enableSubmits); + } + + function buildPlayer() { // extract online presences var op = []; - var presenceTypes = profileUtils.ONLINE_PRESENCE_TYPES; + var presenceTypes = profileUtils.ONLINE_PRESENCE_TYPES; addOnlinePresence(op, $soundCloudUsername.val(), presenceTypes.SOUNDCLOUD.description); addOnlinePresence(op, $reverbNationUsername.val(), presenceTypes.REVERBNATION.description); addOnlinePresence(op, $bandCampUsername.val(), presenceTypes.BANDCAMP.description); @@ -286,15 +307,12 @@ addPerformanceSamples(ps, $jamkazamSampleList, performanceSampleTypes.JAMKAZAM.description); addPerformanceSamples(ps, $soundCloudSampleList, performanceSampleTypes.SOUNDCLOUD.description); addPerformanceSamples(ps, $youTubeSampleList, performanceSampleTypes.YOUTUBE.description); - - api.updateUser({ + + return { website: $website.val(), online_presences: op, performance_samples: ps - }) - .done(postUpdateProfileSuccess) - .fail(postUpdateProfileFailure) - .always(enableSubmits); + } } function postUpdateProfileSuccess(response) { @@ -336,7 +354,7 @@ var initialized = false; $(document).on('JAMKAZAM_READY', function(e, data) { window.JK.JamServer.get$Server().on(window.JK.EVENTS.CONNECTION_UP, function() { - if(initialized) { + if(initialized) { return; } initialized = true; @@ -439,6 +457,10 @@ } // end initializeValidators. + function resetForm() { + $("input", $screen).val("") + } + function initialize() { var screenBindings = { 'beforeShow': beforeShow, @@ -453,6 +475,9 @@ this.initialize = initialize; this.beforeShow = beforeShow; this.afterShow = afterShow; + this.buildPlayer = buildPlayer; + this.renderPlayer = renderPlayer + this.resetForm = resetForm; return this; }; })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 9c87aca44..5e0e71222 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -10,32 +10,39 @@ context.JK.BandSetupScreen = function (app) { var NONE_SPECIFIED = 'None specified' - var GENRE_STEP = 1 + var GENRE_STEP = 1 + var SAMPLE_STEP = 3 + var STEPS_COUNT = 5 + var currentStep = 0 var ui = new context.JK.UIHelper(JK.app) - var logger = context.JK.logger; - var profileUtils = context.JK.ProfileUtils; - var rest = context.JK.Rest(); - var inviteMusiciansUtil = null; - var invitationDialog = null; - var autoComplete = null; - var userNames = []; - var userIds = []; - var userPhotoUrls = []; - var selectedFriendIds = {}; - var nilOptionStr = ''; - var nilOptionText = 'n/a'; - var bandId = ''; - var friendInput=null; - var bandType=null; - var bandStatus=null; - var concertCount=null; - var currentStep = 0; - var STEPS_COUNT=5; + var logger = context.JK.logger + var profileUtils = context.JK.ProfileUtils + var rest = context.JK.Rest() + var inviteMusiciansUtil = null + var invitationDialog = null + var autoComplete = null + var userNames = [] + var userIds = [] + var userPhotoUrls = [] + var selectedFriendIds = {} + var nilOptionStr = '' + var nilOptionText = 'n/a' + var bandId = '' + var friendInput=null + var bandType=null + var bandStatus=null + var concertCount=null + + var $screen=$("#band-setup") var $samples = $screen.find(".account-profile-samples") var $selectedInstruments=[] - var accountProfileSamples = null; - + + console.log("------------------------------------------") + var accountProfileSamples = new JK.AccountProfileSamples(app, $screen, loadBandCallback, rest.updateBand) + accountProfileSamples.initialize() + console.log("==========================================") + function navBack() { if (currentStep>0) { saveBand(function() { @@ -46,7 +53,7 @@ } function navCancel() { - resetForm() + resetForm() window.history.go(-1) return false } @@ -72,6 +79,9 @@ $("#btn-band-setup-back").addClass("hidden") $("#btn-band-setup-next").removeClass("hidden").html("SAVE & NEXT") } else if(currentStepIf your bandmates are already on JamKazam, start typing their names in the box below, or click the Choose Friends button to select them.
    ") invitationDialog = invitationDialogInstance events() diff --git a/web/app/controllers/api_bands_controller.rb b/web/app/controllers/api_bands_controller.rb index 8b0c83ccd..879cb09f2 100644 --- a/web/app/controllers/api_bands_controller.rb +++ b/web/app/controllers/api_bands_controller.rb @@ -250,6 +250,7 @@ class ApiBandsController < ApiController def auth_band_member @band = Band.find(params[:id]) unless @band.users.exists? current_user + Rails.logger.info("Could not find #{current_user} in #{@band.users.inspect}") raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR end end @@ -257,6 +258,7 @@ class ApiBandsController < ApiController uid = current_user.id @band = Band.find(params[:id]) unless @band.band_musicians.detect { |bm| bm.user_id == uid && bm.admin? } + Rails.logger.info("Could not find #{current_user} in #{@band.band_musicians.inspect}") raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR end end diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 5e05e88d6..128a693d1 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -110,6 +110,8 @@ }; } + var api = JK.Rest() + <% if current_user %> JK.currentUserId = '<%= current_user.id %>'; JK.currentUserAvatarUrl = JK.resolveAvatarUrl('<%= current_user.photo_url %>'); @@ -165,7 +167,7 @@ var openBackingTrackDialog = new JK.OpenBackingTrackDialog(JK.app); openBackingTrackDialog.initialize(); - var configureTracksDialog = new JK.ConfigureTracksDialog(JK.app); + var configureTracksDialog = new JK.ConfigureTracksDialog(JK.app, null, api.getUserProfile, api.updateUser) configureTracksDialog.initialize(); var networkTestDialog = new JK.NetworkTestDialog(JK.app); @@ -230,7 +232,7 @@ var accountProfileInterests = new JK.AccountProfileInterests(JK.app); accountProfileInterests.initialize(); - var accountProfileSamples = new JK.AccountProfileSamples(JK.app); + var accountProfileSamples = new JK.AccountProfileSamples(JK.app, null) accountProfileSamples.initialize(); var accountAudioProfile = new JK.AccountAudioProfile(JK.app); From d5a895967742dd7bfd42114f75fc3e0d0d54efd3 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Wed, 3 Jun 2015 14:22:21 -0500 Subject: [PATCH 172/195] Merge with develop. --- admin/app/admin/affiliates.rb | 57 +- admin/app/admin/fraud_alert.rb | 69 + admin/app/admin/machine_extra.rb | 8 + admin/app/admin/machine_fingerprint.rb | 22 + admin/app/admin/music_sessions_comment.rb | 13 + admin/app/admin/session_info_comment.rb | 8 + .../app/views/admin/affiliates/_form.html.erb | 6 - db/manifest | 5 +- db/up/affiliate_partners2.sql | 132 + db/up/optimized_redemption_warn_mode.sql | 42 + ruby/lib/jam_ruby.rb | 12 +- .../lib/jam_ruby/models/affiliate_legalese.rb | 6 + .../models/affiliate_monthly_payment.rb | 39 + ruby/lib/jam_ruby/models/affiliate_partner.rb | 450 +- ruby/lib/jam_ruby/models/affiliate_payment.rb | 49 + .../models/affiliate_quarterly_payment.rb | 41 + .../models/affiliate_referral_visit.rb | 23 + .../models/affiliate_traffic_total.rb | 39 + ruby/lib/jam_ruby/models/anonymous_user.rb | 2 +- .../jam_ruby/models/fingerprint_whitelist.rb | 17 + ruby/lib/jam_ruby/models/fraud_alert.rb | 26 + ruby/lib/jam_ruby/models/generic_state.rb | 5 + ruby/lib/jam_ruby/models/jam_track.rb | 5 + ruby/lib/jam_ruby/models/jam_track_right.rb | 91 +- ruby/lib/jam_ruby/models/machine_extra.rb | 35 + .../jam_ruby/models/machine_fingerprint.rb | 20 +- ruby/lib/jam_ruby/models/music_session.rb | 3 +- .../models/recurly_transaction_web_hook.rb | 7 +- ruby/lib/jam_ruby/models/sale_line_item.rb | 14 +- ruby/lib/jam_ruby/models/user.rb | 20 +- .../resque/scheduled/tally_affiliates.rb | 31 + ruby/spec/factories.rb | 29 + .../jam_ruby/models/affiliate_partner_spec.rb | 831 ++- .../jam_ruby/models/affiliate_payment_spec.rb | 39 + .../models/affiliate_referral_visit_spec.rb | 40 + .../jam_ruby/models/jam_track_right_spec.rb | 110 +- ruby/spec/jam_ruby/models/sale_spec.rb | 142 + ruby/spec/support/utilities.rb | 8 + web/Gemfile | 1 + web/README.md | 1 + web/app/assets/fonts/Raleway/Raleway-Bold.ttf | Bin 0 -> 62224 bytes .../assets/fonts/Raleway/Raleway-Bold.woff | Bin 0 -> 31568 bytes .../assets/fonts/Raleway/Raleway-Bold.woff2 | Bin 0 -> 22592 bytes .../assets/fonts/Raleway/Raleway-Heavy.ttf | Bin 0 -> 63056 bytes .../assets/fonts/Raleway/Raleway-Heavy.woff | Bin 0 -> 29440 bytes .../assets/fonts/Raleway/Raleway-Heavy.woff2 | Bin 0 -> 24156 bytes .../assets/fonts/Raleway/Raleway-Light.ttf | Bin 0 -> 64528 bytes .../assets/fonts/Raleway/Raleway-Light.woff | Bin 0 -> 31592 bytes .../assets/fonts/Raleway/Raleway-Light.woff2 | Bin 0 -> 22628 bytes web/app/assets/fonts/Raleway/Raleway-Thin.ttf | Bin 0 -> 60944 bytes .../assets/fonts/Raleway/Raleway-Thin.woff | Bin 0 -> 27828 bytes .../assets/fonts/Raleway/Raleway-Thin.woff2 | Bin 0 -> 22740 bytes web/app/assets/fonts/Raleway/Raleway.eot | Bin 0 -> 30373 bytes web/app/assets/fonts/Raleway/Raleway.svg | 2168 +++++++ web/app/assets/fonts/Raleway/Raleway.ttf | Bin 0 -> 63796 bytes web/app/assets/fonts/Raleway/Raleway.woff | Bin 0 -> 31836 bytes web/app/assets/fonts/Raleway/Raleway.woff2 | Bin 0 -> 22784 bytes .../assets/images/content/agree_button.png | Bin 0 -> 13570 bytes .../assets/images/content/disagree_button.png | Bin 0 -> 15683 bytes web/app/assets/javascripts/accounts.js | 19 +- .../assets/javascripts/accounts_affiliate.js | 324 ++ web/app/assets/javascripts/application.js | 2 + web/app/assets/javascripts/checkout_order.js | 2 +- .../javascripts/checkout_utils.js.coffee | 6 + web/app/assets/javascripts/corp/corporate.js | 1 + web/app/assets/javascripts/dialog/banner.js | 19 +- .../javascripts/everywhere/everywhere.js | 64 + web/app/assets/javascripts/ga.js | 32 + web/app/assets/javascripts/globals.js | 3 +- .../assets/javascripts/helpBubbleHelper.js | 47 + web/app/assets/javascripts/jam_rest.js | 89 +- .../javascripts/jam_track_preview.js.coffee | 23 +- .../javascripts/jam_track_screen.js.coffee | 115 +- .../javascripts/jamtrack_landing.js.coffee | 3 +- web/app/assets/javascripts/landing/landing.js | 1 + web/app/assets/javascripts/landing/signup.js | 3 + web/app/assets/javascripts/utils.js | 4 + .../javascripts/web/affiliate_links.js.coffee | 55 + .../web/affiliate_program.js.coffee | 119 + .../javascripts/web/individual_jamtrack.js | 8 +- .../web/individual_jamtrack_band.js | 6 +- web/app/assets/javascripts/web/web.js | 4 + .../wizard/gear/step_select_gear.js | 6 +- .../assets/javascripts/wizard/gear_utils.js | 2 +- .../stylesheets/client/account.css.scss | 9 +- .../client/account_affiliate.css.scss | 201 + web/app/assets/stylesheets/client/client.css | 3 + .../assets/stylesheets/client/help.css.scss | 2 + .../client/jamTrackPreview.css.scss | 13 + .../stylesheets/client/jamkazam.css.scss | 12 +- .../stylesheets/client/jamtrack.css.scss | 13 +- .../metronomePlaybackModeSelect.css.scss | 2 +- .../stylesheets/client/muteSelect.css.scss | 2 +- .../stylesheets/client/sessionList.css.scss | 4 +- .../stylesheets/corp/corporate.css.scss.erb | 1 + .../assets/stylesheets/landing/landing.css | 1 + .../landings/affiliate_program.css.scss | 81 + .../landings/partner_agreement_v1.css.scss | 4667 +++++++++++++++ .../stylesheets/minimal/minimal.css.scss | 1 + web/app/assets/stylesheets/web/Raleway.css | 36 + .../stylesheets/web/affiliate_links.css.scss | 65 + web/app/assets/stylesheets/web/web.css | 4 + web/app/controllers/affiliates_controller.rb | 12 + .../controllers/api_affiliate_controller.rb | 53 + web/app/controllers/api_controller.rb | 16 +- web/app/controllers/api_links_controller.rb | 114 + .../api_music_sessions_controller.rb | 3 +- web/app/controllers/api_recurly_controller.rb | 3 +- web/app/controllers/api_users_controller.rb | 39 +- web/app/controllers/application_controller.rb | 22 + web/app/controllers/landings_controller.rb | 5 + web/app/controllers/sessions_controller.rb | 4 +- web/app/controllers/users_controller.rb | 26 +- web/app/helpers/errors_helper.rb | 7 + web/app/helpers/sessions_helper.rb | 1 - .../affiliates/affiliate_links.html.slim | 119 + web/app/views/api_affiliates/create.rabl | 3 + .../views/api_affiliates/monthly_index.rabl | 8 + .../views/api_affiliates/monthly_show.rabl | 3 + .../views/api_affiliates/payment_index.rabl | 8 + .../views/api_affiliates/payment_show.rabl | 3 + .../views/api_affiliates/quarterly_index.rabl | 8 + .../views/api_affiliates/quarterly_show.rabl | 3 + web/app/views/api_affiliates/show.rabl | 2 + .../views/api_affiliates/traffic_index.rabl | 8 + .../views/api_affiliates/traffic_show.rabl | 3 + web/app/views/api_users/show.rabl | 12 + web/app/views/clients/_account.html.erb | 29 + .../_account_affiliate_partner.html.slim | 193 + web/app/views/clients/_help.html.slim | 24 + .../clients/_jam_track_preview.html.slim | 3 +- web/app/views/clients/index.html.erb | 4 + .../landings/affiliate_program.html.slim | 56 + .../landings/individual_jamtrack.html.slim | 1 + .../individual_jamtrack_band.html.slim | 1 + web/app/views/layouts/application.html.erb | 13 - web/app/views/layouts/client.html.erb | 15 - web/app/views/layouts/corporate.html.erb | 15 - web/app/views/layouts/landing.html.erb | 15 - web/app/views/layouts/minimal.html.erb | 15 - web/app/views/layouts/web.html.erb | 15 - .../legal/_partner_agreement_v1.html.erb | 422 ++ .../legal/partner_agreement_full.html.erb | 5062 +++++++++++++++++ web/app/views/shared/_facebook_sdk.html.slim | 2 +- web/app/views/shared/_ga.html.erb | 4 +- .../views/shared/_google_nocaptcha.html.slim | 2 +- web/app/views/shared/_recurly.html.slim | 5 +- web/app/views/users/new.html.erb | 2 +- web/config/application.rb | 7 +- web/config/environments/test.rb | 1 + web/config/initializers/gon.rb | 4 + web/config/routes.rb | 18 + web/config/scheduler.yml | 5 + web/db/schema.rb | 1752 ++++++ web/db/structure.sql | 3862 +++++++++++++ web/lib/tasks/email.rake | 13 + web/lib/tasks/google.rake | 13 + web/lib/tasks/sample_data.rake | 11 + web/lib/user_manager.rb | 4 +- web/loop.bash | 7 + web/public/maintenance.html | 2 +- web/ruby-bt.txt | 919 +++ .../api_affiliate_controller_spec.rb | 82 + .../api_jam_tracks_controller_spec.rb | 17 +- .../controllers/api_links_controller_spec.rb | 106 + web/spec/factories.rb | 33 + web/spec/features/account_affiliate_spec.rb | 152 + web/spec/features/affiliate_program_spec.rb | 90 + web/spec/features/affiliate_referral_spec.rb | 59 + web/spec/features/affiliate_visit_tracking.rb | 36 + web/spec/features/checkout_spec.rb | 117 + web/spec/managers/user_manager_spec.rb | 6 +- web/spec/requests/affilate_referral_spec.rb | 4 +- web/spec/support/app_config.rb | 8 + web/spec/support/remote_ip_monkey_patch.rb | 17 + web/spec/support/utilities.rb | 2 +- web/vendor/assets/javascripts/bugsnag.js | 10 + .../assets/javascripts/jquery.visible.js | 68 + 178 files changed, 24233 insertions(+), 315 deletions(-) create mode 100644 admin/app/admin/fraud_alert.rb create mode 100644 admin/app/admin/machine_extra.rb create mode 100644 admin/app/admin/machine_fingerprint.rb create mode 100644 admin/app/admin/music_sessions_comment.rb create mode 100644 admin/app/admin/session_info_comment.rb create mode 100644 db/up/affiliate_partners2.sql create mode 100644 db/up/optimized_redemption_warn_mode.sql create mode 100644 ruby/lib/jam_ruby/models/affiliate_legalese.rb create mode 100644 ruby/lib/jam_ruby/models/affiliate_monthly_payment.rb create mode 100644 ruby/lib/jam_ruby/models/affiliate_payment.rb create mode 100644 ruby/lib/jam_ruby/models/affiliate_quarterly_payment.rb create mode 100644 ruby/lib/jam_ruby/models/affiliate_referral_visit.rb create mode 100644 ruby/lib/jam_ruby/models/affiliate_traffic_total.rb create mode 100644 ruby/lib/jam_ruby/models/fingerprint_whitelist.rb create mode 100644 ruby/lib/jam_ruby/models/fraud_alert.rb create mode 100644 ruby/lib/jam_ruby/models/machine_extra.rb create mode 100644 ruby/lib/jam_ruby/resque/scheduled/tally_affiliates.rb create mode 100644 ruby/spec/jam_ruby/models/affiliate_payment_spec.rb create mode 100644 ruby/spec/jam_ruby/models/affiliate_referral_visit_spec.rb create mode 100644 web/app/assets/fonts/Raleway/Raleway-Bold.ttf create mode 100644 web/app/assets/fonts/Raleway/Raleway-Bold.woff create mode 100644 web/app/assets/fonts/Raleway/Raleway-Bold.woff2 create mode 100644 web/app/assets/fonts/Raleway/Raleway-Heavy.ttf create mode 100644 web/app/assets/fonts/Raleway/Raleway-Heavy.woff create mode 100644 web/app/assets/fonts/Raleway/Raleway-Heavy.woff2 create mode 100644 web/app/assets/fonts/Raleway/Raleway-Light.ttf create mode 100644 web/app/assets/fonts/Raleway/Raleway-Light.woff create mode 100644 web/app/assets/fonts/Raleway/Raleway-Light.woff2 create mode 100644 web/app/assets/fonts/Raleway/Raleway-Thin.ttf create mode 100644 web/app/assets/fonts/Raleway/Raleway-Thin.woff create mode 100644 web/app/assets/fonts/Raleway/Raleway-Thin.woff2 create mode 100644 web/app/assets/fonts/Raleway/Raleway.eot create mode 100644 web/app/assets/fonts/Raleway/Raleway.svg create mode 100644 web/app/assets/fonts/Raleway/Raleway.ttf create mode 100644 web/app/assets/fonts/Raleway/Raleway.woff create mode 100644 web/app/assets/fonts/Raleway/Raleway.woff2 create mode 100644 web/app/assets/images/content/agree_button.png create mode 100644 web/app/assets/images/content/disagree_button.png create mode 100644 web/app/assets/javascripts/accounts_affiliate.js create mode 100644 web/app/assets/javascripts/web/affiliate_links.js.coffee create mode 100644 web/app/assets/javascripts/web/affiliate_program.js.coffee create mode 100644 web/app/assets/stylesheets/client/account_affiliate.css.scss create mode 100644 web/app/assets/stylesheets/landings/affiliate_program.css.scss create mode 100644 web/app/assets/stylesheets/landings/partner_agreement_v1.css.scss create mode 100644 web/app/assets/stylesheets/web/Raleway.css create mode 100644 web/app/assets/stylesheets/web/affiliate_links.css.scss create mode 100644 web/app/controllers/affiliates_controller.rb create mode 100644 web/app/controllers/api_affiliate_controller.rb create mode 100644 web/app/controllers/api_links_controller.rb create mode 100644 web/app/helpers/errors_helper.rb create mode 100644 web/app/views/affiliates/affiliate_links.html.slim create mode 100644 web/app/views/api_affiliates/create.rabl create mode 100644 web/app/views/api_affiliates/monthly_index.rabl create mode 100644 web/app/views/api_affiliates/monthly_show.rabl create mode 100644 web/app/views/api_affiliates/payment_index.rabl create mode 100644 web/app/views/api_affiliates/payment_show.rabl create mode 100644 web/app/views/api_affiliates/quarterly_index.rabl create mode 100644 web/app/views/api_affiliates/quarterly_show.rabl create mode 100644 web/app/views/api_affiliates/show.rabl create mode 100644 web/app/views/api_affiliates/traffic_index.rabl create mode 100644 web/app/views/api_affiliates/traffic_show.rabl create mode 100644 web/app/views/clients/_account_affiliate_partner.html.slim create mode 100644 web/app/views/landings/affiliate_program.html.slim create mode 100644 web/app/views/legal/_partner_agreement_v1.html.erb create mode 100644 web/app/views/legal/partner_agreement_full.html.erb create mode 100644 web/db/schema.rb create mode 100644 web/db/structure.sql create mode 100644 web/lib/tasks/email.rake create mode 100644 web/lib/tasks/google.rake create mode 100755 web/loop.bash create mode 100644 web/ruby-bt.txt create mode 100644 web/spec/controllers/api_affiliate_controller_spec.rb create mode 100644 web/spec/controllers/api_links_controller_spec.rb create mode 100644 web/spec/features/account_affiliate_spec.rb create mode 100644 web/spec/features/affiliate_program_spec.rb create mode 100644 web/spec/features/affiliate_referral_spec.rb create mode 100644 web/spec/features/affiliate_visit_tracking.rb create mode 100644 web/spec/support/remote_ip_monkey_patch.rb create mode 100644 web/vendor/assets/javascripts/bugsnag.js create mode 100644 web/vendor/assets/javascripts/jquery.visible.js diff --git a/admin/app/admin/affiliates.rb b/admin/app/admin/affiliates.rb index e517b3e12..0409ee36c 100644 --- a/admin/app/admin/affiliates.rb +++ b/admin/app/admin/affiliates.rb @@ -2,48 +2,47 @@ ActiveAdmin.register JamRuby::AffiliatePartner, :as => 'Affiliates' do menu :label => 'Partners', :parent => 'Affiliates' - config.sort_order = 'created_at DESC' + config.sort_order = 'referral_user_count DESC' config.batch_actions = false # config.clear_action_items! config.filters = false form :partial => 'form' + scope("Active", default: true) { |scope| scope.where('partner_user_id IS NOT NULL') } + scope("Unpaid") { |partner| partner.unpaid } + index do - column 'User' do |oo| link_to(oo.partner_user.name, "http://www.jamkazam.com/client#/profile/#{oo.partner_user.id}", {:title => oo.partner_user.name}) end - column 'Email' do |oo| oo.partner_user.email end + + # default_actions # use this for all view/edit/delete links + + column 'User' do |oo| link_to(oo.partner_user.name, admin_user_path(oo.partner_user.id), {:title => oo.partner_user.name}) end column 'Name' do |oo| oo.partner_name end - column 'Code' do |oo| oo.partner_code end + column 'Type' do |oo| oo.entity_type end + column 'Code' do |oo| oo.id end column 'Referral Count' do |oo| oo.referral_user_count end - # column 'Referrals' do |oo| link_to('View', admin_referrals_path(AffiliatePartner::PARAM_REFERRAL => oo.id)) end + column 'Earnings' do |oo| sprintf("$%.2f", oo.cumulative_earnings_in_dollars) end + column 'Amount Owed' do |oo| sprintf("$%.2f", oo.due_amount_in_cents.to_f / 100.to_f) end + column 'Pay Actions' do |oo| + link_to('Mark Paid', mark_paid_admin_affiliate_path(oo.id), :confirm => "Mark this affiliate as PAID?") if oo.unpaid + end + default_actions end + + action_item :only => [:show] do + link_to("Mark Paid", + mark_paid_admin_affiliate_path(resource.id), + :confirm => "Mark this affiliate as PAID?") if resource.unpaid + end + + member_action :mark_paid, :method => :get do + resource.mark_paid + redirect_to admin_affiliate_path(resource.id) + end + controller do - def show - redirect_to admin_referrals_path(AffiliatePartner::PARAM_REFERRAL => resource.id) - end - - def create - obj = AffiliatePartner.create_with_params(params[:jam_ruby_affiliate_partner]) - if obj.errors.present? - set_resource_ivar(obj) - render active_admin_template('new') - else - redirect_to admin_affiliates_path - end - end - - def update - obj = resource - vals = params[:jam_ruby_affiliate_partner] - obj.partner_name = vals[:partner_name] - obj.user_email = vals[:user_email] if vals[:user_email].present? - obj.save! - set_resource_ivar(obj) - render active_admin_template('show') - end - end end diff --git a/admin/app/admin/fraud_alert.rb b/admin/app/admin/fraud_alert.rb new file mode 100644 index 000000000..7c4754a83 --- /dev/null +++ b/admin/app/admin/fraud_alert.rb @@ -0,0 +1,69 @@ +ActiveAdmin.register JamRuby::FraudAlert, :as => 'Fraud Alerts' do + + menu :label => 'Fraud Alerts', :parent => 'JamTracks' + + config.sort_order = 'created_at desc' + config.batch_actions = false + + + scope("Not Whitelisted", default:true) { |scope| + scope.joins('INNER JOIN "machine_fingerprints" ON "machine_fingerprints"."id" = "fraud_alerts"."machine_fingerprint_id" LEFT OUTER JOIN "fingerprint_whitelists" ON "fingerprint_whitelists"."fingerprint" = "machine_fingerprints"."fingerprint"').where('fingerprint_whitelists IS NULL')} + + index do + default_actions + + column :machine_fingerprint + column :user + column :created_at + column :resolved + + column "" do |alert| + link_to 'Matching MAC', "fraud_alerts/#{alert.id}/same_fingerprints" + end + column "" do |alert| + link_to 'Matching MAC and IP Address', "fraud_alerts/#{alert.id}/same_fingerprints_and_ip" + end + column "" do |alert| + link_to 'Matching IP Address', "fraud_alerts/#{alert.id}/same_ip" + end + column "" do |alert| + link_to 'Resolve', "fraud_alerts/#{alert.id}/resolve" + end + column "" do |alert| + link_to 'Whitelist Similar', "fraud_alerts/#{alert.id}/whitelist" + end + end + + member_action :same_fingerprints, :method => :get do + alert = FraudAlert.find(params[:id]) + + redirect_to admin_machine_fingerprints_path("q[fingerprint_equals]" => alert.machine_fingerprint.fingerprint, commit: 'Filter', order: 'created_at_desc') + end + + member_action :same_fingerprints_and_ip, :method => :get do + alert = FraudAlert.find(params[:id]) + + redirect_to admin_machine_fingerprints_path("q[fingerprint_equals]" => alert.machine_fingerprint.fingerprint, "q[remote_ip_equals]" => alert.machine_fingerprint.remote_ip, commit: 'Filter', order: 'created_at_desc') + end + + + member_action :resolve, :method => :get do + alert = FraudAlert.find(params[:id]) + alert.resolved = true + alert.save! + + redirect_to admin_fraud_alerts_path, notice: "That fraud alert has been marked as resolved" + end + + member_action :whitelist, :method => :get do + alert = FraudAlert.find(params[:id]) + + wl = FingerprintWhitelist.new + wl.fingerprint = alert.machine_fingerprint.fingerprint + success = wl.save + + redirect_to admin_fraud_alerts_path, notice: success ? "Added #{alert.machine_fingerprint.fingerprint} to whitelist" : "Could not add #{alert.machine_fingerprint.fingerprint} to whiteliste" + end + + +end \ No newline at end of file diff --git a/admin/app/admin/machine_extra.rb b/admin/app/admin/machine_extra.rb new file mode 100644 index 000000000..c1c5b8738 --- /dev/null +++ b/admin/app/admin/machine_extra.rb @@ -0,0 +1,8 @@ +ActiveAdmin.register JamRuby::MachineExtra, :as => 'Machine Extra' do + + menu :label => 'Machine Extra', :parent => 'JamTracks' + + config.sort_order = 'created_at desc' + config.batch_actions = false + +end \ No newline at end of file diff --git a/admin/app/admin/machine_fingerprint.rb b/admin/app/admin/machine_fingerprint.rb new file mode 100644 index 000000000..4ea9c3c7a --- /dev/null +++ b/admin/app/admin/machine_fingerprint.rb @@ -0,0 +1,22 @@ +ActiveAdmin.register JamRuby::MachineFingerprint, :as => 'Machine Fingerprints' do + + menu :label => 'Machine Fingerprints', :parent => 'JamTracks' + + config.sort_order = 'created_at desc' + config.batch_actions = false + + index do + column :user + column 'Hash' do |fp| + fp.fingerprint + end + column :remote_ip + column 'Detail' do |fp| + detail = fp.detail + if detail + detail.to_s + end + end + column :created_at + end +end \ No newline at end of file diff --git a/admin/app/admin/music_sessions_comment.rb b/admin/app/admin/music_sessions_comment.rb new file mode 100644 index 000000000..5b651a11f --- /dev/null +++ b/admin/app/admin/music_sessions_comment.rb @@ -0,0 +1,13 @@ +ActiveAdmin.register JamRuby::MusicSessionComment, :as => 'Ratings' do + + config.per_page = 150 + config.clear_action_items! + config.sort_order = 'created_at_desc' + menu :parent => 'Sessions', :label => 'Ratings' + + index do + column :comment + column :user + column :created_at + end +end diff --git a/admin/app/admin/session_info_comment.rb b/admin/app/admin/session_info_comment.rb new file mode 100644 index 000000000..cf8fb3c0c --- /dev/null +++ b/admin/app/admin/session_info_comment.rb @@ -0,0 +1,8 @@ +ActiveAdmin.register JamRuby::SessionInfoComment, :as => 'Comments' do + + config.per_page = 50 + config.clear_action_items! + config.sort_order = 'created_at_desc' + menu :parent => 'Sessions', :label => 'Comments' + +end diff --git a/admin/app/views/admin/affiliates/_form.html.erb b/admin/app/views/admin/affiliates/_form.html.erb index a4f14416e..b7a3b047d 100644 --- a/admin/app/views/admin/affiliates/_form.html.erb +++ b/admin/app/views/admin/affiliates/_form.html.erb @@ -1,13 +1,7 @@ <%= semantic_form_for([:admin, resource], :url => resource.new_record? ? admin_affiliates_path : "/admin/affiliates/#{resource.id}") do |f| %> <%= f.semantic_errors *f.object.errors.keys %> <%= f.inputs do %> - <%= f.input(:user_email, :input_html => {:maxlength => 255}) %> <%= f.input(:partner_name, :input_html => {:maxlength => 128}) %> - <% if resource.new_record? %> - <%= f.input(:partner_code, :input_html => {:maxlength => 128}) %> - <% else %> - <%= f.input(:partner_code, :input_html => {:maxlength => 128, :readonly => 'readonly'}) %> - <% end %> <% end %> <%= f.actions %> <% end %> diff --git a/db/manifest b/db/manifest index 3dacdf455..0d4918f5b 100755 --- a/db/manifest +++ b/db/manifest @@ -289,5 +289,8 @@ payment_history.sql jam_track_right_private_key.sql first_downloaded_jamtrack_at.sql signing.sql -enhance_band_profile.sql optimized_redeemption.sql +optimized_redeemption.sql +optimized_redemption_warn_mode.sql +affiliate_partners2.sql +enhance_band_profile.sql \ No newline at end of file diff --git a/db/up/affiliate_partners2.sql b/db/up/affiliate_partners2.sql new file mode 100644 index 000000000..e8488c8ce --- /dev/null +++ b/db/up/affiliate_partners2.sql @@ -0,0 +1,132 @@ +CREATE TABLE affiliate_legalese ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + legalese TEXT, + version INTEGER NOT NULL DEFAULT 1, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE users DROP CONSTRAINT users_affiliate_referral_id_fkey; +ALTER TABLE users DROP COLUMN affiliate_referral_id; +DROP TABLE affiliate_partners; + +CREATE TABLE affiliate_partners ( + id INTEGER PRIMARY KEY, + partner_name VARCHAR(1000), + partner_user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL, + entity_type VARCHAR(64), + legalese_id VARCHAR(64), + signed_at TIMESTAMP, + last_paid_at TIMESTAMP, + address JSON NOT NULL DEFAULT '{}', + tax_identifier VARCHAR(1000), + referral_user_count INTEGER NOT NULL DEFAULT 0, + cumulative_earnings_in_cents INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE SEQUENCE partner_key_sequence; +ALTER SEQUENCE partner_key_sequence RESTART WITH 10000; +ALTER TABLE affiliate_partners ALTER COLUMN id SET DEFAULT nextval('partner_key_sequence');; + +ALTER TABLE users ADD COLUMN affiliate_referral_id INTEGER REFERENCES affiliate_partners(id) ON DELETE SET NULL; +CREATE INDEX affiliate_partners_legalese_idx ON affiliate_partners(legalese_id); + +CREATE UNLOGGED TABLE affiliate_referral_visits ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + affiliate_partner_id INTEGER NOT NULL, + ip_address VARCHAR NOT NULL, + visited_url VARCHAR, + referral_url VARCHAR, + first_visit BOOLEAN NOT NULL DEFAULT TRUE, + user_id VARCHAR(64), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX on affiliate_referral_visits (affiliate_partner_id, created_at); + +CREATE TABLE affiliate_quarterly_payments ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + quarter INTEGER NOT NULL, + year INTEGER NOT NULL, + affiliate_partner_id INTEGER NOT NULL REFERENCES affiliate_partners(id), + due_amount_in_cents INTEGER NOT NULL DEFAULT 0, + paid BOOLEAN NOT NULL DEFAULT FALSE, + closed BOOLEAN NOT NULL DEFAULT FALSE, + jamtracks_sold INTEGER NOT NULL DEFAULT 0, + closed_at TIMESTAMP, + paid_at TIMESTAMP, + last_updated TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +CREATE TABLE affiliate_monthly_payments ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + month INTEGER NOT NULL, + year INTEGER NOT NULL, + affiliate_partner_id INTEGER NOT NULL REFERENCES affiliate_partners(id), + due_amount_in_cents INTEGER NOT NULL DEFAULT 0, + closed BOOLEAN NOT NULL DEFAULT FALSE, + jamtracks_sold INTEGER NOT NULL DEFAULT 0, + closed_at TIMESTAMP, + last_updated TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + + +CREATE INDEX ON affiliate_quarterly_payments (affiliate_partner_id, year, quarter); +CREATE UNIQUE INDEX ON affiliate_quarterly_payments (year, quarter, affiliate_partner_id); +CREATE UNIQUE INDEX ON affiliate_monthly_payments (year, month, affiliate_partner_id); +CREATE INDEX ON affiliate_monthly_payments (affiliate_partner_id, year, month); + + +ALTER TABLE sale_line_items ADD COLUMN affiliate_referral_id INTEGER REFERENCES affiliate_partners(id); +ALTER TABLE sale_line_items ADD COLUMN affiliate_referral_fee_in_cents INTEGER; +ALTER TABLE sale_line_items ADD COLUMN affiliate_refunded BOOLEAN NOT NULL DEFAULT FALSE; +ALTER TABLE sale_line_items ADD COLUMN affiliate_refunded_at TIMESTAMP; +ALTER TABLE generic_state ADD COLUMN affiliate_tallied_at TIMESTAMP; + +CREATE TABLE affiliate_traffic_totals ( + day DATE NOT NULL, + signups INTEGER NOT NULL DEFAULT 0, + visits INTEGER NOT NULL DEFAULT 0, + affiliate_partner_id INTEGER NOT NULL REFERENCES affiliate_partners(id), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX ON affiliate_traffic_totals (day, affiliate_partner_id); +CREATE INDEX ON affiliate_traffic_totals (affiliate_partner_id, day); + +CREATE VIEW affiliate_payments AS + SELECT id AS monthly_id, + CAST(NULL as VARCHAR) AS quarterly_id, + affiliate_partner_id, + due_amount_in_cents, + jamtracks_sold, + created_at, + closed, + CAST(NULL AS BOOLEAN) AS paid, + year, + month as month, + CAST(NULL AS INTEGER) as quarter, + month as time_sort, + 'monthly' AS payment_type + FROM affiliate_monthly_payments + UNION ALL + SELECT CAST(NULL as VARCHAR) AS monthly_id, + id AS quarterly_id, + affiliate_partner_id, + due_amount_in_cents, + jamtracks_sold, + created_at, + closed, + paid, + year, + CAST(NULL AS INTEGER) as month, + quarter, + (quarter * 3) + 3 as time_sort, + 'quarterly' AS payment_type + FROM affiliate_quarterly_payments; \ No newline at end of file diff --git a/db/up/optimized_redemption_warn_mode.sql b/db/up/optimized_redemption_warn_mode.sql new file mode 100644 index 000000000..faf2ad984 --- /dev/null +++ b/db/up/optimized_redemption_warn_mode.sql @@ -0,0 +1,42 @@ +DROP TABLE machine_fingerprints; + +CREATE TABLE machine_fingerprints ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + fingerprint VARCHAR(20000) NOT NULL, + when_taken VARCHAR NOT NULL, + print_type VARCHAR NOT NULL, + remote_ip VARCHAR(1000) NOT NULL, + jam_track_right_id BIGINT REFERENCES jam_track_rights(id) ON DELETE SET NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE machine_extras ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + machine_fingerprint_id VARCHAR(64) NOT NULL REFERENCES machine_fingerprints(id) ON DELETE CASCADE, + mac_address VARCHAR(100), + mac_name VARCHAR(255), + upstate BOOLEAN, + ipaddr_0 VARCHAR(200), + ipaddr_1 VARCHAR(200), + ipaddr_2 VARCHAR(200), + ipaddr_3 VARCHAR(200), + ipaddr_4 VARCHAR(200), + ipaddr_5 VARCHAR(200), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE fraud_alerts ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + machine_fingerprint_id VARCHAR(64) NOT NULL REFERENCES machine_fingerprints(id) ON DELETE CASCADE, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + resolved BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE fingerprint_whitelists ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + fingerprint VARCHAR(20000) UNIQUE NOT NULL +); + +CREATE INDEX machine_fingerprints_index1 ON machine_fingerprints USING btree (fingerprint, user_id, remote_ip, created_at); diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index a098341a6..4e57b4813 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -59,8 +59,9 @@ require "jam_ruby/resque/scheduled/score_history_sweeper" require "jam_ruby/resque/scheduled/scheduled_music_session_cleaner" require "jam_ruby/resque/scheduled/recordings_cleaner" require "jam_ruby/resque/scheduled/jam_tracks_cleaner" -require "jam_ruby/resque/jam_tracks_builder" require "jam_ruby/resque/scheduled/stats_maker" +require "jam_ruby/resque/scheduled/tally_affiliates" +require "jam_ruby/resque/jam_tracks_builder" require "jam_ruby/resque/google_analytics_event" require "jam_ruby/resque/batch_email_job" require "jam_ruby/resque/long_running" @@ -104,6 +105,9 @@ require "jam_ruby/models/user" require "jam_ruby/models/anonymous_user" require "jam_ruby/models/signup_hint" require "jam_ruby/models/machine_fingerprint" +require "jam_ruby/models/machine_extra" +require "jam_ruby/models/fraud_alert" +require "jam_ruby/models/fingerprint_whitelist" require "jam_ruby/models/rsvp_request" require "jam_ruby/models/rsvp_slot" require "jam_ruby/models/rsvp_request_rsvp_slot" @@ -203,6 +207,12 @@ require "jam_ruby/app/mailers/async_mailer" require "jam_ruby/app/mailers/batch_mailer" require "jam_ruby/app/mailers/progress_mailer" require "jam_ruby/models/affiliate_partner" +require "jam_ruby/models/affiliate_legalese" +require "jam_ruby/models/affiliate_quarterly_payment" +require "jam_ruby/models/affiliate_monthly_payment" +require "jam_ruby/models/affiliate_traffic_total" +require "jam_ruby/models/affiliate_referral_visit" +require "jam_ruby/models/affiliate_payment" require "jam_ruby/models/chat_message" require "jam_ruby/models/shopping_cart" require "jam_ruby/models/generic_state" diff --git a/ruby/lib/jam_ruby/models/affiliate_legalese.rb b/ruby/lib/jam_ruby/models/affiliate_legalese.rb new file mode 100644 index 000000000..732d45025 --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_legalese.rb @@ -0,0 +1,6 @@ +class JamRuby::AffiliateLegalese < ActiveRecord::Base + self.table_name = 'affiliate_legalese' + + has_many :affiliate_partners, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :legalese_id + +end diff --git a/ruby/lib/jam_ruby/models/affiliate_monthly_payment.rb b/ruby/lib/jam_ruby/models/affiliate_monthly_payment.rb new file mode 100644 index 000000000..8aa45ca5a --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_monthly_payment.rb @@ -0,0 +1,39 @@ +class JamRuby::AffiliateMonthlyPayment < ActiveRecord::Base + + belongs_to :affiliate_partner, class_name: 'JamRuby::AffiliatePartner', inverse_of: :months + + + def self.index(user, options) + unless user.affiliate_partner + return [[], nil] + end + + page = options[:page].to_i + per_page = options[:per_page].to_i + + if page == 0 + page = 1 + end + + if per_page == 0 + per_page = 50 + end + + start = (page -1 ) * per_page + limit = per_page + + query = AffiliateMonthlyPayment + .paginate(page: page, per_page: per_page) + .where(affiliate_partner_id: user.affiliate_partner.id) + .order('year ASC, month ASC') + + if query.length == 0 + [query, nil] + elsif query.length < limit + [query, nil] + else + [query, start + limit] + end + end + +end diff --git a/ruby/lib/jam_ruby/models/affiliate_partner.rb b/ruby/lib/jam_ruby/models/affiliate_partner.rb index 6df9dc1a2..4408ffa42 100644 --- a/ruby/lib/jam_ruby/models/affiliate_partner.rb +++ b/ruby/lib/jam_ruby/models/affiliate_partner.rb @@ -1,25 +1,75 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base - belongs_to :partner_user, :class_name => "JamRuby::User", :foreign_key => :partner_user_id - has_many :user_referrals, :class_name => "JamRuby::User", :foreign_key => :affiliate_referral_id + self.table_name = 'affiliate_partners' + belongs_to :partner_user, :class_name => "JamRuby::User", :foreign_key => :partner_user_id, inverse_of: :affiliate_partner + has_many :user_referrals, :class_name => "JamRuby::User", :foreign_key => :affiliate_referral_id + belongs_to :affiliate_legalese, :class_name => "JamRuby::AffiliateLegalese", :foreign_key => :legalese_id + has_many :sale_line_items, :class_name => 'JamRuby::SaleLineItem', foreign_key: :affiliate_referral_id + has_many :quarters, :class_name => 'JamRuby::AffiliateQuarterlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner + has_many :months, :class_name => 'JamRuby::AffiliateMonthlyPayment', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner + has_many :traffic_totals, :class_name => 'JamRuby::AffiliateTrafficTotal', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner + has_many :visits, :class_name => 'JamRuby::AffiliateReferralVisit', foreign_key: :affiliate_partner_id, inverse_of: :affiliate_partner attr_accessible :partner_name, :partner_code, :partner_user_id + ENTITY_TYPES = %w{ Individual Sole\ Proprietor Limited\ Liability\ Company\ (LLC) Partnership Trust/Estate S\ Corporation C\ Corporation Other } + + KEY_ADDR1 = 'address1' + KEY_ADDR2 = 'address2' + KEY_CITY = 'city' + KEY_STATE = 'state' + KEY_POSTAL = 'postal_code' + KEY_COUNTRY = 'country' + + # ten dollars in cents + PAY_THRESHOLD = 10 * 100 + + AFFILIATE_PARAMS="utm_source=affiliate&utm_medium=affiliate&utm_campaign=2015-affiliate-custom&affiliate=" + + ADDRESS_SCHEMA = { + KEY_ADDR1 => '', + KEY_ADDR2 => '', + KEY_CITY => '', + KEY_STATE => '', + KEY_POSTAL => '', + KEY_COUNTRY => '', + } + PARAM_REFERRAL = :ref PARAM_COOKIE = :affiliate_ref PARTNER_CODE_REGEX = /^[#{Regexp.escape('abcdefghijklmnopqrstuvwxyz0123456789-._+,')}]+{2,128}$/i - validates :user_email, format: {with: JamRuby::User::VALID_EMAIL_REGEX}, :if => :user_email - validates :partner_name, presence: true - validates :partner_code, presence: true, format: { with: PARTNER_CODE_REGEX } - validates :partner_user, presence: true + #validates :user_email, format: {with: JamRuby::User::VALID_EMAIL_REGEX}, :if => :user_email + #validates :partner_code, format: { with: PARTNER_CODE_REGEX }, :allow_blank => true + validates :entity_type, inclusion: {in: ENTITY_TYPES, message: "invalid entity type"} + serialize :address, JSON + + before_save do |record| + record.address ||= ADDRESS_SCHEMA.clone + record.entity_type ||= ENTITY_TYPES.first + end + + # used by admin def self.create_with_params(params={}) + raise 'not supported' oo = self.new oo.partner_name = params[:partner_name].try(:strip) oo.partner_code = params[:partner_code].try(:strip).try(:downcase) oo.partner_user = User.where(:email => params[:user_email].try(:strip)).limit(1).first oo.partner_user_id = oo.partner_user.try(:id) + oo.entity_type = params[:entity_type] || ENTITY_TYPES.first + oo.save + oo + end + + # used by web + def self.create_with_web_params(user, params={}) + oo = self.new + oo.partner_name = params[:partner_name].try(:strip) + oo.partner_user = user if user # user is not required + oo.entity_type = params[:entity_type] || ENTITY_TYPES.first + oo.signed_at = Time.now oo.save oo end @@ -29,16 +79,394 @@ class JamRuby::AffiliatePartner < ActiveRecord::Base end def self.is_code?(code) - self.where(:partner_code => code).limit(1).pluck(:id).present? + self.where(:partner_code => code).limit(1).pluck(:id).present? end def referrals_by_date by_date = User.where(:affiliate_referral_id => self.id) - .group('DATE(created_at)') - .having("COUNT(*) > 0") - .order('date_created_at DESC') - .count + .group('DATE(created_at)') + .having("COUNT(*) > 0") + .order('date_created_at DESC') + .count block_given? ? yield(by_date) : by_date end + def signed_legalese(legalese) + self.affiliate_legalese = legalese + self.signed_at = Time.now + save! + end + + def update_address_value(key, val) + self.address[key] = val + self.update_attribute(:address, self.address) + end + + def address_value(key) + self.address[key] + end + + def created_within_affiliate_window(user, sale_time) + sale_time - user.created_at < 2.years + end + + def should_attribute_sale?(shopping_cart) + if shopping_cart.is_jam_track? + if created_within_affiliate_window(shopping_cart.user, Time.now) + product_info = shopping_cart.product_info + # subtract the total quantity from the freebie quantity, to see how much we should attribute to them + real_quantity = product_info[:quantity].to_i - product_info[:marked_for_redeem].to_i + {fee_in_cents: real_quantity * 20} + else + false + end + else + raise 'shopping cart type not implemented yet' + end + end + + def cumulative_earnings_in_dollars + cumulative_earnings_in_cents.to_f / 100.to_f + end + + def self.quarter_info(date) + + year = date.year + + # which quarter? + quarter = -1 + if date.month >= 1 && date.month <= 3 + quarter = 0 + elsif date.month >= 4 && date.month <= 6 + quarter = 1 + elsif date.month >= 7 && date.month <= 9 + quarter = 2 + elsif date.month >= 10 && date.month <= 12 + quarter = 3 + end + + raise 'quarter should never be -1' if quarter == -1 + + previous_quarter = quarter - 1 + previous_year = date.year + if previous_quarter == -1 + previous_quarter = 3 + previous_year = year - 1 + end + + raise 'previous quarter should never be -1' if previous_quarter == -1 + + {year: year, quarter: quarter, previous_quarter: previous_quarter, previous_year: previous_year} + end + + def self.did_quarter_elapse?(quarter_info, last_tallied_info) + if last_tallied_info.nil? + true + else + quarter_info == last_tallied_info + end + end + + # meant to be run regularly; this routine will make summarized counts in the + # AffiliateQuarterlyPayment table + # AffiliatePartner.cumulative_earnings_in_cents, AffiliatePartner.referral_user_count + def self.tally_up(day) + + AffiliatePartner.transaction do + quarter_info = quarter_info(day) + last_tallied_info = quarter_info(GenericState.affiliate_tallied_at) if GenericState.affiliate_tallied_at + + quarter_elapsed = did_quarter_elapse?(quarter_info, last_tallied_info) + + if quarter_elapsed + tally_monthly_payments(quarter_info[:previous_year], quarter_info[:previous_quarter]) + tally_quarterly_payments(quarter_info[:previous_year], quarter_info[:previous_quarter]) + end + tally_monthly_payments(quarter_info[:year], quarter_info[:quarter]) + tally_quarterly_payments(quarter_info[:year], quarter_info[:quarter]) + + tally_traffic_totals(GenericState.affiliate_tallied_at, day) + + tally_partner_totals + + state = GenericState.singleton + state.affiliate_tallied_at = day + state.save! + end + + end + + # this just makes sure that the quarter rows exist before later manipulations with UPDATEs + def self.ensure_quarters_exist(year, quarter) + + sql = %{ + INSERT INTO affiliate_quarterly_payments (quarter, year, affiliate_partner_id) + (SELECT #{quarter}, #{year}, affiliate_partners.id FROM affiliate_partners WHERE affiliate_partners.partner_user_id IS NOT NULL AND affiliate_partners.id NOT IN + (SELECT affiliate_partner_id FROM affiliate_quarterly_payments WHERE year = #{year} AND quarter = #{quarter})) + } + + ActiveRecord::Base.connection.execute(sql) + end + + # this just makes sure that the quarter rows exist before later manipulations with UPDATEs + def self.ensure_months_exist(year, quarter) + + months = [1, 2, 3].collect! { |i| quarter * 3 + i } + + months.each do |month| + sql = %{ + INSERT INTO affiliate_monthly_payments (month, year, affiliate_partner_id) + (SELECT #{month}, #{year}, affiliate_partners.id FROM affiliate_partners WHERE affiliate_partners.partner_user_id IS NOT NULL AND affiliate_partners.id NOT IN + (SELECT affiliate_partner_id FROM affiliate_monthly_payments WHERE year = #{year} AND month = #{month})) + } + + ActiveRecord::Base.connection.execute(sql) + end + end + + + def self.sale_items_subquery(start_date, end_date, table_name) + %{ + FROM sale_line_items + WHERE + (DATE(sale_line_items.created_at) >= DATE('#{start_date}') AND DATE(sale_line_items.created_at) <= DATE('#{end_date}')) + AND + sale_line_items.affiliate_referral_id = #{table_name}.affiliate_partner_id + } + end + + def self.sale_items_refunded_subquery(start_date, end_date, table_name) + %{ + FROM sale_line_items + WHERE + (DATE(sale_line_items.affiliate_refunded_at) >= DATE('#{start_date}') AND DATE(sale_line_items.affiliate_refunded_at) <= DATE('#{end_date}')) + AND + sale_line_items.affiliate_referral_id = #{table_name}.affiliate_partner_id + AND + sale_line_items.affiliate_refunded = TRUE + } + end + # total up quarters by looking in sale_line_items for items that are marked as having a affiliate_referral_id + # don't forget to substract any sale_line_items that have a affiliate_refunded = TRUE + def self.total_months(year, quarter) + + months = [1, 2, 3].collect! { |i| quarter * 3 + i } + + + months.each do |month| + + start_date, end_date = boundary_dates_for_month(year, month) + + sql = %{ + UPDATE affiliate_monthly_payments + SET + last_updated = NOW(), + jamtracks_sold = + COALESCE( + (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + #{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')} + ), 0) + + + COALESCE( + (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')} + ), 0), + due_amount_in_cents = + COALESCE( + (SELECT SUM(affiliate_referral_fee_in_cents) + #{sale_items_subquery(start_date, end_date, 'affiliate_monthly_payments')} + ), 0) + + + COALESCE( + (SELECT -SUM(affiliate_referral_fee_in_cents) + #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_monthly_payments')} + ), 0) + + WHERE closed = FALSE AND year = #{year} AND month = #{month} + } + + ActiveRecord::Base.connection.execute(sql) + end + end + + # close any quarters that are done, so we don't manipulate them again + def self.close_months(year, quarter) + # close any quarters that occurred before this quarter + month = quarter * 3 + 1 + + sql = %{ + UPDATE affiliate_monthly_payments + SET + closed = TRUE, closed_at = NOW() + WHERE year < #{year} OR month < #{month} + } + + ActiveRecord::Base.connection.execute(sql) + + end + + # total up quarters by looking in sale_line_items for items that are marked as having a affiliate_referral_id + # don't forget to substract any sale_line_items that have a affiliate_refunded = TRUE + def self.total_quarters(year, quarter) + start_date, end_date = boundary_dates(year, quarter) + + sql = %{ + UPDATE affiliate_quarterly_payments + SET + last_updated = NOW(), + jamtracks_sold = + COALESCE( + (SELECT COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + #{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')} + ), 0) + + + COALESCE( + (SELECT -COUNT(CASE WHEN sale_line_items.product_type = 'JamTrack' AND sale_line_items.affiliate_referral_fee_in_cents > 0 THEN 1 ELSE NULL END) + #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')} + ), 0), + due_amount_in_cents = + COALESCE( + (SELECT SUM(affiliate_referral_fee_in_cents) + #{sale_items_subquery(start_date, end_date, 'affiliate_quarterly_payments')} + ), 0) + + + COALESCE( + (SELECT -SUM(affiliate_referral_fee_in_cents) + #{sale_items_refunded_subquery(start_date, end_date, 'affiliate_quarterly_payments')} + ), 0) + + WHERE closed = FALSE AND paid = FALSE AND year = #{year} AND quarter = #{quarter} + } + + ActiveRecord::Base.connection.execute(sql) + end + + # close any quarters that are done, so we don't manipulate them again + def self.close_quarters(year, quarter) + # close any quarters that occurred before this quarter + sql = %{ + UPDATE affiliate_quarterly_payments + SET + closed = TRUE, closed_at = NOW() + WHERE year < #{year} OR quarter < #{quarter} + } + + ActiveRecord::Base.connection.execute(sql) + end + + def self.tally_quarterly_payments(year, quarter) + ensure_quarters_exist(year, quarter) + + total_quarters(year, quarter) + + close_quarters(year, quarter) + end + + + def self.tally_monthly_payments(year, quarter) + ensure_months_exist(year, quarter) + + total_months(year, quarter) + + close_months(year, quarter) + end + + def self.tally_partner_totals + sql = %{ + UPDATE affiliate_partners SET + referral_user_count = (SELECT count(*) FROM users WHERE affiliate_partners.id = users.affiliate_referral_id), + cumulative_earnings_in_cents = (SELECT COALESCE(SUM(due_amount_in_cents), 0) FROM affiliate_quarterly_payments AS aqp WHERE aqp.affiliate_partner_id = affiliate_partners.id AND closed = TRUE and paid = TRUE) + } + ActiveRecord::Base.connection.execute(sql) + end + + def self.tally_traffic_totals(last_tallied_at, target_day) + + if last_tallied_at + start_date = last_tallied_at.to_date + end_date = target_day.to_date + else + start_date = target_day.to_date - 1 + end_date = target_day.to_date + end + + if start_date == end_date + return + end + + sql = %{ + INSERT INTO affiliate_traffic_totals(SELECT day, 0, 0, ap.id FROM affiliate_partners AS ap CROSS JOIN (select (generate_series('#{start_date}', '#{end_date - 1}', '1 day'::interval))::date as day) AS lurp) + } + ActiveRecord::Base.connection.execute(sql) + + sql = %{ + UPDATE affiliate_traffic_totals traffic SET visits = COALESCE((SELECT COALESCE(count(affiliate_partner_id), 0) FROM affiliate_referral_visits v WHERE DATE(v.created_at) >= DATE('#{start_date}') AND DATE(v.created_at) < DATE('#{end_date}') AND v.created_at::date = traffic.day AND v.affiliate_partner_id = traffic.affiliate_partner_id GROUP BY affiliate_partner_id, v.created_at::date ), 0) WHERE traffic.day >= DATE('#{start_date}') AND traffic.day < DATE('#{end_date}') + } + ActiveRecord::Base.connection.execute(sql) + + sql = %{ + UPDATE affiliate_traffic_totals traffic SET signups = COALESCE((SELECT COALESCE(count(v.id), 0) FROM users v WHERE DATE(v.created_at) >= DATE('#{start_date}') AND DATE(v.created_at) < DATE('#{end_date}') AND v.created_at::date = traffic.day AND v.affiliate_referral_id = traffic.affiliate_partner_id GROUP BY affiliate_referral_id, v.created_at::date ), 0) WHERE traffic.day >= DATE('#{start_date}') AND traffic.day < DATE('#{end_date}') + } + ActiveRecord::Base.connection.execute(sql) + end + + def self.boundary_dates(year, quarter) + if quarter == 0 + [Date.new(year, 1, 1), Date.new(year, 3, 31)] + elsif quarter == 1 + [Date.new(year, 4, 1), Date.new(year, 6, 30)] + elsif quarter == 2 + [Date.new(year, 7, 1), Date.new(year, 9, 30)] + elsif quarter == 3 + [Date.new(year, 10, 1), Date.new(year, 12, 31)] + else + raise "invalid quarter #{quarter}" + end + end + + # 1-based month + def self.boundary_dates_for_month(year, month) + [Date.new(year, month, 1), Date.civil(year, month, -1)] + end + + # Finds all affiliates that need to be paid + def self.unpaid + + joins(:quarters) + .where('affiliate_quarterly_payments.paid = false').where('affiliate_quarterly_payments.closed = true') + .group('affiliate_partners.id') + .having('sum(due_amount_in_cents) >= ?', PAY_THRESHOLD) + .order('sum(due_amount_in_cents) DESC') + + end + + # does this one affiliate need to be paid? + def unpaid + due_amount_in_cents > PAY_THRESHOLD + end + + # admin function: mark the affiliate paid + def mark_paid + if unpaid + transaction do + now = Time.now + quarters.where(paid:false, closed:true).update_all(paid:true, paid_at: now) + self.last_paid_at = now + self.save! + end + end + end + + # how much is this affiliate due? + def due_amount_in_cents + total_in_cents = 0 + quarters.where(paid:false, closed:true).each do |quarter| + total_in_cents = total_in_cents + quarter.due_amount_in_cents + end + total_in_cents + end + + def affiliate_query_params + AffiliatePartner::AFFILIATE_PARAMS + self.id.to_s + end end diff --git a/ruby/lib/jam_ruby/models/affiliate_payment.rb b/ruby/lib/jam_ruby/models/affiliate_payment.rb new file mode 100644 index 000000000..1aa29f1c8 --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_payment.rb @@ -0,0 +1,49 @@ +module JamRuby + class AffiliatePayment < ActiveRecord::Base + + belongs_to :affiliate_monthly_payment + belongs_to :affiliate_quarterly_payment + + def self.index(user, options) + + unless user.affiliate_partner + return [[], nil] + end + + affiliate_partner_id = user.affiliate_partner.id + + + page = options[:page].to_i + per_page = options[:per_page].to_i + + if page == 0 + page = 1 + end + + if per_page == 0 + per_page = 50 + end + + start = (page -1 ) * per_page + limit = per_page + + + query = AffiliatePayment + .includes(affiliate_quarterly_payment: [], affiliate_monthly_payment:[]) + .where(affiliate_partner_id: affiliate_partner_id) + .where("(payment_type='quarterly' AND closed = true) OR payment_type='monthly'") + .where('(paid = TRUE or due_amount_in_cents < 10000 or paid is NULL)') + .paginate(:page => page, :per_page => limit) + .order('year ASC, time_sort ASC, payment_type ASC') + + if query.length == 0 + [query, nil] + elsif query.length < limit + [query, nil] + else + [query, start + limit] + end + end + end +end + diff --git a/ruby/lib/jam_ruby/models/affiliate_quarterly_payment.rb b/ruby/lib/jam_ruby/models/affiliate_quarterly_payment.rb new file mode 100644 index 000000000..caf9eced7 --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_quarterly_payment.rb @@ -0,0 +1,41 @@ +class JamRuby::AffiliateQuarterlyPayment < ActiveRecord::Base + + belongs_to :affiliate_partner, class_name: 'JamRuby::AffiliatePartner', inverse_of: :quarters + + + def self.index(user, options) + unless user.affiliate_partner + return [[], nil] + end + + page = options[:page].to_i + per_page = options[:per_page].to_i + + if page == 0 + page = 1 + end + + if per_page == 0 + per_page = 50 + end + + start = (page -1 ) * per_page + limit = per_page + + query = AffiliateQuarterlyPayment + .paginate(page: page, per_page: per_page) + .where(affiliate_partner_id: user.affiliate_partner.id) + .where(closed:true) + .where(paid:true) + .order('year ASC, quarter ASC') + + if query.length == 0 + [query, nil] + elsif query.length < limit + [query, nil] + else + [query, start + limit] + end + end + +end diff --git a/ruby/lib/jam_ruby/models/affiliate_referral_visit.rb b/ruby/lib/jam_ruby/models/affiliate_referral_visit.rb new file mode 100644 index 000000000..cf9e9fbc0 --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_referral_visit.rb @@ -0,0 +1,23 @@ +class JamRuby::AffiliateReferralVisit < ActiveRecord::Base + + belongs_to :affiliate_partner, class_name: 'JamRuby::AffiliatePartner', inverse_of: :visits + + validates :affiliate_partner_id, numericality: {only_integer: true}, :allow_nil => true + validates :visited_url, length: {maximum: 1000} + validates :referral_url, length: {maximum: 1000} + validates :ip_address, presence: true, length: {maximum:1000} + validates :first_visit, inclusion: {in: [true, false]} + validates :user_id, length: {maximum:64} + + def self.track(options = {}) + visit = AffiliateReferralVisit.new + visit.affiliate_partner_id = options[:affiliate_id] + visit.ip_address = options[:remote_ip] + visit.visited_url = options[:visited_url] + visit.referral_url = options[:referral_url] + visit.first_visit = options[:visited].nil? + visit.user_id = options[:current_user].id if options[:current_user] + visit.save + visit + end +end diff --git a/ruby/lib/jam_ruby/models/affiliate_traffic_total.rb b/ruby/lib/jam_ruby/models/affiliate_traffic_total.rb new file mode 100644 index 000000000..c85fa4fb5 --- /dev/null +++ b/ruby/lib/jam_ruby/models/affiliate_traffic_total.rb @@ -0,0 +1,39 @@ +class JamRuby::AffiliateTrafficTotal < ActiveRecord::Base + + belongs_to :affiliate_partner, class_name: 'JamRuby::AffiliatePartner', inverse_of: :traffic_totals + + + def self.index(user, options) + unless user.affiliate_partner + return [[], nil] + end + + page = options[:page].to_i + per_page = options[:per_page].to_i + + if page == 0 + page = 1 + end + + if per_page == 0 + per_page = 50 + end + + start = (page -1 ) * per_page + limit = per_page + + query = AffiliateTrafficTotal + .paginate(page: page, per_page: per_page) + .where(affiliate_partner_id: user.affiliate_partner.id) + .where('visits != 0 OR signups != 0') + .order('day ASC') + + if query.length == 0 + [query, nil] + elsif query.length < limit + [query, nil] + else + [query, start + limit] + end + end +end diff --git a/ruby/lib/jam_ruby/models/anonymous_user.rb b/ruby/lib/jam_ruby/models/anonymous_user.rb index ff7b60730..24bc2b104 100644 --- a/ruby/lib/jam_ruby/models/anonymous_user.rb +++ b/ruby/lib/jam_ruby/models/anonymous_user.rb @@ -28,7 +28,7 @@ module JamRuby end def signup_hint - SignupHint.find_by_anonymous_user_id(@id) + SignupHint.where(anonymous_user_id: @id).where('expires_at > ?', Time.now).first end end end diff --git a/ruby/lib/jam_ruby/models/fingerprint_whitelist.rb b/ruby/lib/jam_ruby/models/fingerprint_whitelist.rb new file mode 100644 index 000000000..88f015aa3 --- /dev/null +++ b/ruby/lib/jam_ruby/models/fingerprint_whitelist.rb @@ -0,0 +1,17 @@ +module JamRuby + class FingerprintWhitelist < ActiveRecord::Base + + @@log = Logging.logger[FingerprintWhitelist] + + validates :fingerprint, presence: true, uniqueness: true + has_many :machine_fingerprint, class_name: 'JamRuby::MachineFingerprint', foreign_key: :fingerprint + + def admin_url + APP_CONFIG.admin_root_url + "/admin/fingerprint_whitelists/" + id + end + + def to_s + "#{fingerprint}" + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/fraud_alert.rb b/ruby/lib/jam_ruby/models/fraud_alert.rb new file mode 100644 index 000000000..344eb7ddc --- /dev/null +++ b/ruby/lib/jam_ruby/models/fraud_alert.rb @@ -0,0 +1,26 @@ +module JamRuby + class FraudAlert < ActiveRecord::Base + + @@log = Logging.logger[MachineExtra] + + belongs_to :machine_fingerprint, :class_name => "JamRuby::MachineFingerprint" + belongs_to :user, :class_name => "JamRuby::User" + + + def self.create(machine_fingerprint, user) + fraud = FraudAlert.new + fraud.machine_fingerprint = machine_fingerprint + fraud.user = user + fraud.save + + unless fraud.save + @@log.error("unable to create fraud: #{fraud.errors.inspect}") + end + fraud + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/fraud_alerts/" + id + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/generic_state.rb b/ruby/lib/jam_ruby/models/generic_state.rb index d6c0bc687..6ed4ec7f4 100644 --- a/ruby/lib/jam_ruby/models/generic_state.rb +++ b/ruby/lib/jam_ruby/models/generic_state.rb @@ -23,9 +23,14 @@ module JamRuby (database_environment == 'development' && Environment.mode == 'development') end + def self.affiliate_tallied_at + GenericState.singleton.affiliate_tallied_at + end + def self.singleton GenericState.find('default') end + end end diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index ae465b2f2..8ab120c62 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -263,5 +263,10 @@ module JamRuby jam_track_rights.where("user_id=?", user).first end + def short_plan_code + prefix = 'jamtrack-' + plan_code[prefix.length..-1] + end + end end diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb index fda1ecb97..f0749dc94 100644 --- a/ruby/lib/jam_ruby/models/jam_track_right.rb +++ b/ruby/lib/jam_ruby/models/jam_track_right.rb @@ -229,8 +229,8 @@ module JamRuby return "no fingerprint specified" end - all_fingerprint = fingerprint[:all] - running_fingerprint = fingerprint[:running] + all_fingerprint = fingerprint.delete(:all) + running_fingerprint = fingerprint.delete(:running) if all_fingerprint.blank? return "no all fingerprint specified" @@ -240,6 +240,9 @@ module JamRuby return "no running fingerprint specified" end + all_fingerprint_extra = fingerprint[all_fingerprint] + running_fingerprint_extra = fingerprint[running_fingerprint] + if redeemed && !redeemed_and_fingerprinted # if this is a free JamTrack, we need to check for fraud or accidental misuse @@ -250,41 +253,80 @@ module JamRuby return "already redeemed another" end - # can we find a jam track that belongs to someone else with the same fingerprint - match = MachineFingerprint.find_by_fingerprint(all_fingerprint) + if FingerprintWhitelist.select('id').find_by_fingerprint(all_fingerprint) + # we can short circuit out of the rest of the check, since this is a known bad fingerprint + @@log.debug("ignoring 'all' hash found in whitelist") + else + # can we find a jam track that belongs to someone else with the same fingerprint + conflict = MachineFingerprint.select('count(id) as count').where('user_id != ?', current_user.id).where(fingerprint: all_fingerprint).where(remote_ip: remote_ip).where('created_at > ?', APP_CONFIG.expire_fingerprint_days.days.ago).first + conflict_count = conflict['count'].to_i - if match && match.user != current_user - AdminMailer.alerts(subject: "'All' fingerprint collision by #{current_user.name}", - body: "MachineFingerprint #{match.inspect}\n\nCurrent User: #{current_user.admin_url}").deliver + if conflict_count >= APP_CONFIG.found_conflict_count + mf = MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, all_fingerprint_extra, self) + + # record the alert + fraud = FraudAlert.create(mf, current_user) if mf.valid? + fraud_admin_url = fraud.admin_url if fraud + + + AdminMailer.alerts(subject: "'All' fingerprint collision by #{current_user.name}", + body: "Current User: #{current_user.admin_url}\n\n Fraud Alert: #{fraud_admin_url}").deliver + + # try to record the other fingerprint + mf = MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self) + + if APP_CONFIG.error_on_fraud + return "other user has 'all' fingerprint" + else + self.redeemed_and_fingerprinted = true + save! + return nil + end - # try to record the other fingerprint - MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, self) - if APP_CONFIG.error_on_fraud - return "other user has 'all' fingerprint" end + end - if all_fingerprint != running_fingerprint - match = MachineFingerprint.find_by_fingerprint(running_fingerprint) - if match && match.user != current_user - AdminMailer.alerts(subject: "'Running' fingerprint collision by #{current_user.name}", - body: "MachineFingerprint #{match.inspect}\n\nCurrent User: #{current_user.admin_url}").deliver - # try to record the other fingerprint - MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, self) - if APP_CONFIG.error_on_fraud - return "other user has 'running' fingerprint" + if all_fingerprint != running_fingerprint + if FingerprintWhitelist.select('id').find_by_fingerprint(running_fingerprint) + # we can short circuit out of the rest of the check, since this is a known bad fingerprint + @@log.debug("ignoring 'running' hash found in whitelist") + else + + conflict = MachineFingerprint.select('count(id) as count').where('user_id != ?', current_user.id).where(fingerprint: running_fingerprint).where(remote_ip: remote_ip).where('created_at > ?', APP_CONFIG.expire_fingerprint_days.days.ago).first + conflict_count = conflict['count'].to_i + if conflict_count >= APP_CONFIG.found_conflict_count + mf = MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self) + + # record the alert + fraud = FraudAlert.create(mf, current_user) if mf.valid? + fraud_admin_url = fraud.admin_url if fraud + AdminMailer.alerts(subject: "'Running' fingerprint collision by #{current_user.name}", + body: "Current User: #{current_user.admin_url}\n\nFraud Alert: #{fraud_admin_url}").deliver\ + + # try to record the other fingerprint + mf = MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, all_fingerprint_extra, self) + + + if APP_CONFIG.error_on_fraud + return "other user has 'running' fingerprint" + else + self.redeemed_and_fingerprinted = true + save! + return nil + end end end + end # we made it past all checks; let's slap on the redeemed_fingerprint self.redeemed_and_fingerprinted = true - - MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, self) + MachineFingerprint.create(all_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ALL, remote_ip, all_fingerprint_extra, self) if all_fingerprint != running_fingerprint - MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, self) + MachineFingerprint.create(running_fingerprint, current_user, MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD, MachineFingerprint::PRINT_TYPE_ACTIVE, remote_ip, running_fingerprint_extra, self) end save! @@ -297,11 +339,12 @@ module JamRuby def self.stats stats = {} - result = JamTrackRight.select('count(id) as total, count(CASE WHEN signing_44 THEN 1 ELSE NULL END) + count(CASE WHEN signing_48 THEN 1 ELSE NULL END) as signing_count, count(CASE WHEN redeemed THEN 1 ELSE NULL END) as redeem_count').where(is_test_purchase: false).first + result = JamTrackRight.select('count(id) as total, count(CASE WHEN signing_44 THEN 1 ELSE NULL END) + count(CASE WHEN signing_48 THEN 1 ELSE NULL END) as signing_count, count(CASE WHEN redeemed THEN 1 ELSE NULL END) as redeem_count, count(last_downloaded_at) as redeemed_and_dl_count').where(is_test_purchase: false).first stats['count'] = result['total'].to_i stats['signing_count'] = result['signing_count'].to_i stats['redeemed_count'] = result['redeem_count'].to_i + stats['redeemed_and_dl_count'] = result['redeemed_and_dl_count'].to_i stats['purchased_count'] = stats['count'] - stats['redeemed_count'] stats end diff --git a/ruby/lib/jam_ruby/models/machine_extra.rb b/ruby/lib/jam_ruby/models/machine_extra.rb new file mode 100644 index 000000000..74b1402d0 --- /dev/null +++ b/ruby/lib/jam_ruby/models/machine_extra.rb @@ -0,0 +1,35 @@ +module JamRuby + class MachineExtra < ActiveRecord::Base + + @@log = Logging.logger[MachineExtra] + + belongs_to :machine_fingerprint, :class_name => "JamRuby::MachineFingerprint" + + def self.create(machine_fingerprint, data) + me = MachineExtra.new + me.machine_fingerprint = machine_fingerprint + me.mac_address = data[:mac] + me.mac_name = data[:name] + me.upstate = data[:upstate] + me.ipaddr_0 = data[:ipaddr_0] + me.ipaddr_1 = data[:ipaddr_1] + me.ipaddr_2 = data[:ipaddr_2] + me.ipaddr_3 = data[:ipaddr_3] + me.ipaddr_4 = data[:ipaddr_4] + me.ipaddr_5 = data[:ipaddr_5] + me.save + + unless me.save + @@log.error("unable to create machine extra: #{me.errors.inspect}") + end + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/machine_extras/" + id + end + + def to_s + "#{mac_address} #{mac_name} #{upstate ? 'UP' : 'DOWN'} #{ipaddr_0} #{ipaddr_1} #{ipaddr_2} #{ipaddr_3} #{ipaddr_4} #{ipaddr_5}" + end + end +end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/machine_fingerprint.rb b/ruby/lib/jam_ruby/models/machine_fingerprint.rb index 7d0076e50..d4d668052 100644 --- a/ruby/lib/jam_ruby/models/machine_fingerprint.rb +++ b/ruby/lib/jam_ruby/models/machine_fingerprint.rb @@ -5,6 +5,8 @@ module JamRuby belongs_to :user, :class_name => "JamRuby::User" belongs_to :jam_track_right, :class_name => "JamRuby::JamTrackRight" + has_one :detail, :class_name => "JamRuby::MachineExtra" + belongs_to :fingerprint_whitelist, class_name: 'JamRuby::FingerprintWhitelist', foreign_key: :fingerprint TAKEN_ON_SUCCESSFUL_DOWNLOAD = 'dl' TAKEN_ON_FRAUD_CONFLICT = 'fc' @@ -15,11 +17,11 @@ module JamRuby validates :user, presence:true validates :when_taken, :inclusion => {:in => [TAKEN_ON_SUCCESSFUL_DOWNLOAD, TAKEN_ON_FRAUD_CONFLICT]} - validates :fingerprint, presence: true, uniqueness:true + validates :fingerprint, presence: true validates :print_type, presence: true, :inclusion => {:in =>[PRINT_TYPE_ALL, PRINT_TYPE_ACTIVE]} validates :remote_ip, presence: true - def self.create(fingerprint, user, when_taken, print_type, remote_ip, jam_track_right = nil) + def self.create(fingerprint, user, when_taken, print_type, remote_ip, extra, jam_track_right = nil) mf = MachineFingerprint.new mf.fingerprint = fingerprint mf.user = user @@ -27,9 +29,21 @@ module JamRuby mf.print_type = print_type mf.remote_ip = remote_ip mf.jam_track_right = jam_track_right - unless mf.save + if mf.save + MachineExtra.create(mf, extra) if extra + else + @@log.error("unable to create machine fingerprint: #{mf.errors.inspect}") end + mf + end + + def admin_url + APP_CONFIG.admin_root_url + "/admin/machine_fingerprints/" + id + end + + def to_s + "#{fingerprint} #{remote_ip} #{user} #{detail}" end end end \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index e2954d9f3..96b9b7831 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -282,7 +282,7 @@ module JamRuby return query end - def self.scheduled user + def self.scheduled user, only_public = false # keep unstarted sessions around for 12 hours after scheduled_start session_not_started = "(music_sessions.scheduled_start > NOW() - '12 hour'::INTERVAL AND music_sessions.started_at IS NULL)" @@ -293,6 +293,7 @@ module JamRuby session_finished = "(music_sessions.session_removed_at > NOW() - '2 hour'::INTERVAL)" query = MusicSession.where("music_sessions.canceled = FALSE") + query = query.where('music_sessions.fan_access = TRUE or music_sessions.musician_access = TRUE') if only_public query = query.where("music_sessions.user_id = '#{user.id}'") query = query.where("music_sessions.scheduled_start IS NULL OR #{session_not_started} OR #{session_finished} OR #{session_started_not_finished}") query = query.where("music_sessions.create_type IS NULL OR music_sessions.create_type != '#{CREATE_TYPE_QUICK_START}'") diff --git a/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb index 05824e322..9d0fc9bd4 100644 --- a/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb +++ b/ruby/lib/jam_ruby/models/recurly_transaction_web_hook.rb @@ -100,9 +100,14 @@ module JamRuby if sale && sale.is_jam_track_sale? if sale.sale_line_items.length == 1 if sale.recurly_total_in_cents == transaction.amount_in_cents - jam_track = sale.sale_line_items[0].product + line_item = sale.sale_line_items[0] + jam_track = line_item.product jam_track_right = jam_track.right_for_user(transaction.user) if jam_track if jam_track_right + line_item.affiliate_refunded = true + line_item.affiliate_refunded_at = Time.now + line_item.save! + jam_track_right.destroy # associate which JamTrack we assume this is related to in this one success case diff --git a/ruby/lib/jam_ruby/models/sale_line_item.rb b/ruby/lib/jam_ruby/models/sale_line_item.rb index 2022318b0..7d744cfe5 100644 --- a/ruby/lib/jam_ruby/models/sale_line_item.rb +++ b/ruby/lib/jam_ruby/models/sale_line_item.rb @@ -8,7 +8,8 @@ module JamRuby belongs_to :sale, class_name: 'JamRuby::Sale' belongs_to :jam_track, class_name: 'JamRuby::JamTrack' belongs_to :jam_track_right, class_name: 'JamRuby::JamTrackRight' - has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale_line_item, foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid' + belongs_to :affiliate_referral, class_name: 'JamRuby::AffiliatePartner', foreign_key: :affiliate_referral_id + has_many :recurly_transactions, class_name: 'JamRuby::RecurlyTransactionWebHook', inverse_of: :sale_line_item, foreign_key: 'subscription_id', primary_key: 'recurly_subscription_uuid' validates :product_type, inclusion: {in: [JAMBLASTER, JAMCLOUD, JAMTRACK]} validates :unit_price, numericality: {only_integer: false} @@ -16,6 +17,7 @@ module JamRuby validates :free, numericality: {only_integer: true} validates :sales_tax, numericality: {only_integer: false}, allow_nil: true validates :shipping_handling, numericality: {only_integer: false} + validates :affiliate_referral_fee_in_cents, numericality: {only_integer: false}, allow_nil: true validates :recurly_plan_code, presence:true validates :sale, presence:true @@ -76,6 +78,16 @@ module JamRuby sale_line_item.recurly_subscription_uuid = recurly_subscription_uuid sale_line_item.recurly_adjustment_uuid = recurly_adjustment_uuid sale_line_item.recurly_adjustment_credit_uuid = recurly_adjustment_credit_uuid + + # determine if we need to associate this sale with a partner + user = shopping_cart.user + referral_info = user.should_attribute_sale?(shopping_cart) + + if referral_info + sale_line_item.affiliate_referral = user.affiliate_referral + sale_line_item.affiliate_referral_fee_in_cents = referral_info[:fee_in_cents] + end + sale.sale_line_items << sale_line_item sale_line_item.save sale_line_item diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index e70fafe68..ecd0a3501 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -144,7 +144,7 @@ module JamRuby has_many :event_sessions, :class_name => "JamRuby::EventSession" # affiliate_partner - has_one :affiliate_partner, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :partner_user_id + has_one :affiliate_partner, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :partner_user_id, inverse_of: :partner_user belongs_to :affiliate_referral, :class_name => "JamRuby::AffiliatePartner", :foreign_key => :affiliate_referral_id, :counter_cache => :referral_user_count # diagnostics has_many :diagnostics, :class_name => "JamRuby::Diagnostic" @@ -991,6 +991,7 @@ module JamRuby any_user = options[:any_user] reuse_card = options[:reuse_card] signup_hint = options[:signup_hint] + affiliate_partner = options[:affiliate_partner] user = User.new @@ -1125,6 +1126,14 @@ module JamRuby if user.errors.any? raise ActiveRecord::Rollback else + # if the partner ID was present and the partner doesn't already have a user associated, associate this new user with the affiliate partner + if affiliate_partner && affiliate_partner.partner_user.nil? + affiliate_partner.partner_user = user + unless affiliate_partner.save + @@log.error("unable to associate #{user.to_s} with affiliate_partner #{affiliate_partner.id} / #{affiliate_partner.partner_name}") + end + end + if user.affiliate_referral = AffiliatePartner.find_by_id(affiliate_referral_id) user.save end if affiliate_referral_id.present? @@ -1640,6 +1649,15 @@ module JamRuby options end + def should_attribute_sale?(shopping_cart) + if affiliate_referral + referral_info = affiliate_referral.should_attribute_sale?(shopping_cart) + else + false + end + + + end private def create_remember_token self.remember_token = SecureRandom.urlsafe_base64 diff --git a/ruby/lib/jam_ruby/resque/scheduled/tally_affiliates.rb b/ruby/lib/jam_ruby/resque/scheduled/tally_affiliates.rb new file mode 100644 index 000000000..2327f281f --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/tally_affiliates.rb @@ -0,0 +1,31 @@ +require 'json' +require 'resque' +require 'resque-retry' +require 'net/http' +require 'digest/md5' + +module JamRuby + + # periodically scheduled to find jobs that need retrying + class TallyAffiliates + extend Resque::Plugins::JamLonelyJob + + @queue = :tally_affiliates + + @@log = Logging.logger[TallyAffiliates] + + def self.lock_timeout + # this should be enough time to make sure the job has finished, but not so long that the system isn't recovering from a abandoned job + 120 + end + + def self.perform + @@log.debug("waking up") + + AffiliatePartner.tally_up(Date.today) + + @@log.debug("done") + end + end + +end \ No newline at end of file diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index bfdbde5f4..e2a45863d 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -797,4 +797,33 @@ FactoryGirl.define do transaction_type JamRuby::RecurlyTransactionWebHook::FAILED_PAYMENT end end + + factory :affiliate_partner, class: 'JamRuby::AffiliatePartner' do + sequence(:partner_name) { |n| "partner-#{n}" } + entity_type 'Individual' + signed_at Time.now + association :partner_user, factory: :user + end + + factory :affiliate_quarterly_payment, class: 'JamRuby::AffiliateQuarterlyPayment' do + year 2015 + quarter 0 + association :affiliate_partner, factory: :affiliate_partner + end + + factory :affiliate_monthly_payment, class: 'JamRuby::AffiliateMonthlyPayment' do + year 2015 + month 0 + association :affiliate_partner, factory: :affiliate_partner + end + + factory :affiliate_referral_visit, class: 'JamRuby::AffiliateReferralVisit' do + ip_address '1.1.1.1' + association :affiliate_partner, factory: :affiliate_partner + end + + factory :affiliate_legalese, class: 'JamRuby::AffiliateLegalese' do + legalese Faker::Lorem.paragraphs(6).join("\n\n") + end + end diff --git a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb index 9b25931d9..822a1f527 100644 --- a/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb +++ b/ruby/spec/jam_ruby/models/affiliate_partner_spec.rb @@ -3,47 +3,46 @@ require 'spec_helper' describe AffiliatePartner do let!(:user) { FactoryGirl.create(:user) } - let!(:partner) { - AffiliatePartner.create_with_params({:partner_name => 'partner', - :partner_code => 'code', - :user_email => user.email}) - } + let(:partner) { FactoryGirl.create(:affiliate_partner) } + let!(:legalese) { FactoryGirl.create(:affiliate_legalese) } + let(:jam_track) {FactoryGirl.create(:jam_track) } - # Faker::Lorem.word is tripping up the PARTNER_CODE_REGEX. We should not use it. - it 'validates required fields' do - pending - expect(partner.referral_user_count).to eq(0) - expect(partner.partner_user).to eq(user) - user.reload - expect(user.affiliate_partner).to eq(partner) + describe "unpaid" do + it "succeeds with no data" do + AffiliatePartner.unpaid.length.should eq(0) + end - oo = AffiliatePartner.create_with_params({:partner_name => Faker::Company.name, - :partner_code => 'a', - :user_email => user.email}) - expect(oo.errors.messages[:partner_code][0]).to eq('is invalid') - oo = AffiliatePartner.create_with_params({:partner_name => Faker::Company.name, - :partner_code => 'foo bar', - :user_email => user.email}) - expect(oo.errors.messages[:partner_code][0]).to eq('is invalid') - oo = AffiliatePartner.create_with_params({:partner_name => '', - :partner_code => Faker::Lorem.word, - :user_email => user.email}) - expect(oo.errors.messages[:partner_name][0]).to eq("can't be blank") - oo = AffiliatePartner.create_with_params({:partner_name => '', - :partner_code => Faker::Lorem.word, - :user_email => Faker::Internet.email}) - expect(oo.errors.messages[:partner_user][0]).to eq("can't be blank") + it "finds one unpaid partner" do + quarter = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner, closed:true, paid:false, due_amount_in_cents: AffiliatePartner::PAY_THRESHOLD) + AffiliatePartner.unpaid.should eq([partner]) + end - code = Faker::Lorem.word.upcase - oo = AffiliatePartner.create_with_params({:partner_name => Faker::Company.name, - :partner_code => " #{code} ", - :user_email => user.email}) - expect(oo.partner_code).to eq(code.downcase) + it "finds one unpaid partner with two quarters that exceed threshold" do + # this $5 quarter is not enough to make the threshold + quarter = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner, year:2016, closed:true, paid:false, due_amount_in_cents: AffiliatePartner::PAY_THRESHOLD / 2) + AffiliatePartner.unpaid.should eq([]) + + # this should get the user over the hump + quarter = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner, year:2015, closed:true, paid:false, due_amount_in_cents: AffiliatePartner::PAY_THRESHOLD / 2) + AffiliatePartner.unpaid.should eq([partner]) + end + + it "does not find paid or closed quarters" do + quarter = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner, year:2016, closed:true, paid:true, due_amount_in_cents: AffiliatePartner::PAY_THRESHOLD) + AffiliatePartner.unpaid.should eq([]) + + quarter = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner, year:2015, closed:false, paid:true, due_amount_in_cents: AffiliatePartner::PAY_THRESHOLD) + AffiliatePartner.unpaid.should eq([]) + end + end + + it "user-partner association" do + user_partner = FactoryGirl.create(:user, affiliate_partner: partner) + user_partner.affiliate_partner.should_not be_nil + user_partner.affiliate_partner.present?.should be_true end it 'has user referrals' do - pending - expect(AffiliatePartner.coded_id(partner.partner_code)).to eq(partner.id) expect(partner.referral_user_count).to eq(0) uu = FactoryGirl.create(:user) uu.affiliate_referral = partner @@ -73,4 +72,766 @@ describe AffiliatePartner do expect(by_date[keys.last]).to eq(2) end + it 'updates address correctly' do + addy = partner.address.clone + addy[AffiliatePartner::KEY_ADDR1] = Faker::Address.street_address + addy[AffiliatePartner::KEY_ADDR2] = Faker::Address.secondary_address + addy[AffiliatePartner::KEY_CITY] = Faker::Address.city + addy[AffiliatePartner::KEY_STATE] = Faker::Address.state_abbr + addy[AffiliatePartner::KEY_COUNTRY] = Faker::Address.country + partner.update_address_value(AffiliatePartner::KEY_ADDR1, addy[AffiliatePartner::KEY_ADDR1]) + partner.update_address_value(AffiliatePartner::KEY_ADDR2, addy[AffiliatePartner::KEY_ADDR2]) + partner.update_address_value(AffiliatePartner::KEY_CITY, addy[AffiliatePartner::KEY_CITY]) + partner.update_address_value(AffiliatePartner::KEY_STATE, addy[AffiliatePartner::KEY_STATE]) + partner.update_address_value(AffiliatePartner::KEY_COUNTRY, addy[AffiliatePartner::KEY_COUNTRY]) + + expect(partner.address[AffiliatePartner::KEY_ADDR1]).to eq(addy[AffiliatePartner::KEY_ADDR1]) + expect(partner.address[AffiliatePartner::KEY_ADDR2]).to eq(addy[AffiliatePartner::KEY_ADDR2]) + expect(partner.address[AffiliatePartner::KEY_CITY]).to eq(addy[AffiliatePartner::KEY_CITY]) + expect(partner.address[AffiliatePartner::KEY_STATE]).to eq(addy[AffiliatePartner::KEY_STATE]) + expect(partner.address[AffiliatePartner::KEY_COUNTRY]).to eq(addy[AffiliatePartner::KEY_COUNTRY]) + end + + it 'associates legalese' do + + end + + describe "should_attribute_sale?" do + + it "user with no affiliate relationship" do + shopping_cart = ShoppingCart.create user, jam_track, 1 + user.should_attribute_sale?(shopping_cart).should be_false + end + + it "user with an affiliate relationship buying a jamtrack" do + user.affiliate_referral = partner + user.save! + shopping_cart = ShoppingCart.create user, jam_track, 1, false + user.should_attribute_sale?(shopping_cart).should eq({fee_in_cents:20}) + end + + it "user with an affiliate relationship redeeming a jamtrack" do + user.affiliate_referral = partner + user.save! + shopping_cart = ShoppingCart.create user, jam_track, 1, true + user.should_attribute_sale?(shopping_cart).should eq({fee_in_cents:0}) + end + + it "user with an expired affiliate relationship redeeming a jamtrack" do + user.affiliate_referral = partner + user.created_at = (365 * 2 + 1).days.ago + user.save! + shopping_cart = ShoppingCart.create user, jam_track, 1, false + user.should_attribute_sale?(shopping_cart).should be_false + end + end + + describe "created_within_affiliate_window" do + it "user created very recently" do + partner.created_within_affiliate_window(user, Time.now).should be_true + end + + it "user created 2 years, 1 day asgo" do + days_future = 365 * 2 + 1 + + partner.created_within_affiliate_window(user, days_future.days.from_now).should be_false + end + end + + describe "tally_up" do + let(:partner1) {FactoryGirl.create(:affiliate_partner)} + let(:partner2) {FactoryGirl.create(:affiliate_partner)} + let(:payment1) {FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner1)} + let(:user_partner1) { FactoryGirl.create(:user, affiliate_referral: partner1)} + let(:user_partner2) { FactoryGirl.create(:user, affiliate_referral: partner2)} + let(:sale) {Sale.create_jam_track_sale(user_partner1)} + + describe "ensure_quarters_exist" do + + it "runs OK with no data" do + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliateQuarterlyPayment.count.should eq(0) + end + + it "creates new slots" do + partner1.touch + partner2.touch + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliateQuarterlyPayment.count.should eq(2) + quarter = partner1.quarters.first + quarter.year.should eq(2015) + quarter.quarter.should eq(0) + quarter = partner2.quarters.first + quarter.year.should eq(2015) + quarter.quarter.should eq(0) + end + + it "creates one slot, ignoring other" do + partner1.touch + partner2.touch + payment1.touch + AffiliateQuarterlyPayment.count.should eq(1) + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliateQuarterlyPayment.count.should eq(2) + quarter = partner1.quarters.first + quarter.year.should eq(2015) + quarter.quarter.should eq(0) + quarter = partner2.quarters.first + quarter.year.should eq(2015) + quarter.quarter.should eq(0) + end + end + + describe "close_quarters" do + it "runs OK with no data" do + AffiliateQuarterlyPayment.count.should eq(0) + AffiliatePartner.close_quarters(2015, 1) + end + + it "ignores current quarter" do + partner1.touch + partner2.touch + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliateQuarterlyPayment.count.should eq(2) + AffiliatePartner.close_quarters(2015, 0) + AffiliateQuarterlyPayment.where(closed: true).count.should eq(0) + end + + it "closes previous quarter" do + partner1.touch + partner2.touch + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliateQuarterlyPayment.count.should eq(2) + AffiliatePartner.close_quarters(2015, 1) + AffiliateQuarterlyPayment.where(closed: true).count.should eq(2) + end + + it "closes previous quarter (edge case)" do + partner1.touch + partner2.touch + AffiliatePartner.ensure_quarters_exist(2014, 3) + AffiliateQuarterlyPayment.count.should eq(2) + AffiliatePartner.close_quarters(2015, 0) + AffiliateQuarterlyPayment.where(closed: true).count.should eq(2) + end + end + + describe "tally_partner_totals" do + it "runs OK with no data" do + AffiliatePartner.tally_partner_totals + AffiliatePartner.count.should eq(0) + end + + it "updates partner when there is no data to tally" do + partner1.touch + partner1.cumulative_earnings_in_cents.should eq(0) + partner1.referral_user_count.should eq(0) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.cumulative_earnings_in_cents.should eq(0) + partner1.referral_user_count.should eq(0) + end + + it "updates referral_user_count" do + FactoryGirl.create(:user, affiliate_referral: partner1) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(1) + partner1.cumulative_earnings_in_cents.should eq(0) + + FactoryGirl.create(:user, affiliate_referral: partner2) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(1) + partner1.cumulative_earnings_in_cents.should eq(0) + partner2.reload + partner2.referral_user_count.should eq(1) + partner2.cumulative_earnings_in_cents.should eq(0) + + FactoryGirl.create(:user, affiliate_referral: partner2) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(1) + partner1.cumulative_earnings_in_cents.should eq(0) + partner2.reload + partner2.referral_user_count.should eq(2) + partner2.cumulative_earnings_in_cents.should eq(0) + end + + it "updates cumulative_earnings_in_cents" do + FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner:partner1, year:2015, quarter:0, due_amount_in_cents: 0, closed:true, paid:true) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(0) + partner1.cumulative_earnings_in_cents.should eq(0) + + FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner:partner2, year:2015, quarter:0, due_amount_in_cents: 10, closed:true, paid:true) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(0) + partner1.cumulative_earnings_in_cents.should eq(0) + partner2.reload + partner2.referral_user_count.should eq(0) + partner2.cumulative_earnings_in_cents.should eq(10) + + FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner:partner2, year:2015, quarter:1, due_amount_in_cents: 100, closed:true, paid:true) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(0) + partner1.cumulative_earnings_in_cents.should eq(0) + partner2.reload + partner2.referral_user_count.should eq(0) + partner2.cumulative_earnings_in_cents.should eq(110) + + FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner:partner1, year:2015, quarter:1, due_amount_in_cents: 100, closed:true, paid:true) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(0) + partner1.cumulative_earnings_in_cents.should eq(100) + partner2.reload + partner2.referral_user_count.should eq(0) + partner2.cumulative_earnings_in_cents.should eq(110) + + # a paid=false quarterly payment does not yet reflect in cumulative earnings + FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner:partner1, year:2015, quarter:2, due_amount_in_cents: 1000, closed:false, paid:false) + AffiliatePartner.tally_partner_totals + partner1.reload + partner1.referral_user_count.should eq(0) + partner1.cumulative_earnings_in_cents.should eq(100) + partner2.reload + partner2.referral_user_count.should eq(0) + partner2.cumulative_earnings_in_cents.should eq(110) + end + end + + describe "total_quarters" do + it "runs OK with no data" do + AffiliateQuarterlyPayment.count.should eq(0) + AffiliatePartner.total_quarters(2015, 0) + end + + it "totals 0 with no sales data" do + partner1.touch + partner2.touch + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliateQuarterlyPayment.count.should eq(2) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(0) + quarter.last_updated.should_not be_nil + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(0) + quarter.last_updated.should_not be_nil + end + + it "totals with sales data" do + partner1.touch + partner2.touch + + + # create a freebie for partner1 + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, true + freebie_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + freebie_sale.affiliate_referral_fee_in_cents.should eq(0) + freebie_sale.created_at = Date.new(2015, 1, 1) + freebie_sale.save! + + + + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliateQuarterlyPayment.count.should eq(2) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(0) + quarter.jamtracks_sold.should eq(0) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(0) + quarter.jamtracks_sold.should eq(0) + + # create a real sale for partner1 + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2015, 1, 1) + real_sale.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(0) + quarter.jamtracks_sold.should eq(0) + + # create a real sale for partner2 + shopping_cart = ShoppingCart.create user_partner2, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2015, 1, 1) + real_sale.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + + # create a real sale for partner1 + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2015, 1, 1) + real_sale.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(40) + quarter.jamtracks_sold.should eq(2) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + + + # create a real sale for a non-affiliated user + shopping_cart = ShoppingCart.create user, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should be_nil + real_sale.created_at = Date.new(2015, 1, 1) + real_sale.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(40) + quarter.jamtracks_sold.should eq(2) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + + # create a real sale but in previous quarter (should no have effect on the quarter being computed) + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2014, 12, 31) + real_sale.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(40) + quarter.jamtracks_sold.should eq(2) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + + # create a real sale but in later quarter (should no have effect on the quarter being computed) + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2015, 4, 1) + real_sale.save! + real_sale_later = real_sale + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(40) + quarter.jamtracks_sold.should eq(2) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + + # create a real sale but then refund it + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2015, 3, 31) + real_sale.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(60) + quarter.jamtracks_sold.should eq(3) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(20) + # now refund it + real_sale.affiliate_refunded_at = Date.new(2015, 3, 1) + real_sale.affiliate_refunded = true + real_sale.save! + AffiliatePartner.ensure_quarters_exist(2015, 0) + AffiliatePartner.total_quarters(2015, 0) + quarter = partner1.quarters.first + quarter.due_amount_in_cents.should eq(40) + quarter = partner2.quarters.first + quarter.due_amount_in_cents.should eq(20) + quarter.jamtracks_sold.should eq(1) + + + # create the 2nd quarter, which should add up the sale created a few bits up + AffiliatePartner.ensure_quarters_exist(2015, 1) + AffiliatePartner.total_quarters(2015, 1) + payment = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(1, 2015, partner1.id) + payment.due_amount_in_cents.should eq(20) + + # and now refund it in the 3rd quarter + real_sale_later.affiliate_refunded_at = Date.new(2015, 7, 1) + real_sale_later.affiliate_refunded = true + real_sale_later.save! + AffiliatePartner.total_quarters(2015, 1) + payment = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(1, 2015, partner1.id) + payment.due_amount_in_cents.should eq(20) + payment.jamtracks_sold.should eq(1) + + # now catch the one refund in the 3rd quarter + AffiliatePartner.ensure_quarters_exist(2015, 2) + AffiliatePartner.total_quarters(2015, 2) + payment = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(2, 2015, partner1.id) + payment.due_amount_in_cents.should eq(-20) + payment.jamtracks_sold.should eq(-1) + + end + end + + describe "tally_up complete" do + it "runs OK with no data" do + AffiliatePartner.tally_up(Date.new(2015, 1, 1)) + end + + it "successive runs" do + GenericState.singleton.affiliate_tallied_at.should be_nil + AffiliatePartner.tally_up(Date.new(2015, 1, 1)) + GenericState.singleton.affiliate_tallied_at.should_not be_nil + AffiliateQuarterlyPayment.count.should eq(0) + + # partner is created + partner1.touch + + AffiliatePartner.tally_up(Date.new(2015, 1, 1)) + AffiliateQuarterlyPayment.count.should eq(2) + AffiliateMonthlyPayment.count.should eq(6) + + quarter_previous = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(3, 2014, partner1.id) + quarter_previous.due_amount_in_cents.should eq(0) + quarter_previous.closed.should be_true + quarter = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(0, 2015, partner1.id) + quarter.due_amount_in_cents.should eq(0) + quarter.closed.should be_false + + month_previous= AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(10, 2014, partner1.id) + month_previous.due_amount_in_cents.should eq(0) + month_previous.closed.should be_true + month_previous.jamtracks_sold.should eq(0) + month_previous= AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(11, 2014, partner1.id) + month_previous.due_amount_in_cents.should eq(0) + month_previous.closed.should be_true + month_previous.jamtracks_sold.should eq(0) + month_previous= AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(12, 2014, partner1.id) + month_previous.due_amount_in_cents.should eq(0) + month_previous.closed.should be_true + month_previous.jamtracks_sold.should eq(0) + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(1, 2015, partner1.id) + month_previous.due_amount_in_cents.should eq(0) + month_previous.closed.should be_true + month_previous.jamtracks_sold.should eq(0) + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(2, 2015, partner1.id) + month_previous.due_amount_in_cents.should eq(0) + month_previous.closed.should be_true + month.jamtracks_sold.should eq(0) + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(3, 2015, partner1.id) + month_previous.due_amount_in_cents.should eq(0) + month_previous.closed.should be_true + month_previous.jamtracks_sold.should eq(0) + + + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2015, 4, 1) + real_sale.save! + + AffiliatePartner.tally_up(Date.new(2015, 4, 1)) + AffiliateQuarterlyPayment.count.should eq(3) + quarter = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(0, 2015, partner1.id) + quarter.due_amount_in_cents.should eq(0) + quarter.jamtracks_sold.should eq(0) + quarter.closed.should be_true + quarter2 = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(1, 2015, partner1.id) + quarter2.due_amount_in_cents.should eq(20) + quarter2.jamtracks_sold.should eq(1) + quarter2.closed.should be_false + + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(1, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.jamtracks_sold.should eq(0) + month.closed.should be_true + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(2, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.jamtracks_sold.should eq(0) + month.closed.should be_true + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(3, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.jamtracks_sold.should eq(0) + month.closed.should be_true + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(4, 2015, partner1.id) + month.due_amount_in_cents.should eq(20) + month.jamtracks_sold.should eq(1) + month.closed.should be_false + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(5, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.jamtracks_sold.should eq(0) + month.closed.should be_false + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(6, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.jamtracks_sold.should eq(0) + month.closed.should be_false + + # now sneak in a purchase in the 1st quarter, which makes no sense, but proves that closed quarters are not touched + + shopping_cart = ShoppingCart.create user_partner1, jam_track, 1, false + real_sale = SaleLineItem.create_from_shopping_cart(sale, shopping_cart, nil, nil, nil) + real_sale.affiliate_referral_fee_in_cents.should eq(20) + real_sale.created_at = Date.new(2015, 1, 1) + real_sale.save! + + AffiliatePartner.tally_up(Date.new(2015, 4, 2)) + quarter = AffiliateQuarterlyPayment.find_by_quarter_and_year_and_affiliate_partner_id!(0, 2015, partner1.id) + quarter.due_amount_in_cents.should eq(0) + quarter.jamtracks_sold.should eq(0) + quarter.closed.should be_true + + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(1, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.closed.should be_true + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(2, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.jamtracks_sold.should eq(0) + month.closed.should be_true + month = AffiliateMonthlyPayment.find_by_month_and_year_and_affiliate_partner_id!(3, 2015, partner1.id) + month.due_amount_in_cents.should eq(0) + month.jamtracks_sold.should eq(0) + month.closed.should be_true + end + end + + describe "tally_traffic_totals" do + + it "runs OK with no data" do + AffiliatePartner.tally_traffic_totals(Date.yesterday, Date.today) + end + + it "can deal with simple signup case" do + user_partner1.touch + + day0 = user_partner1.created_at.to_date + + # simulate what happens when this scheduled job first ever runs + AffiliatePartner.tally_traffic_totals(nil, day0) + AffiliateTrafficTotal.count.should eq(1) + traffic_total_day_before = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0 - 1, user_partner1.affiliate_referral_id) + traffic_total_day_before.visits.should eq(0) + traffic_total_day_before.signups.should eq(0) + + # then simulate when it runs on the same day as it ran on the day before + AffiliatePartner.tally_traffic_totals(day0, day0) + AffiliateTrafficTotal.count.should eq(1) + traffic_total_day_before = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0 - 1, user_partner1.affiliate_referral_id) + traffic_total_day_before.visits.should eq(0) + traffic_total_day_before.signups.should eq(0) + + # now run it on the next day, which should catch the signup event + + day1 = day0 + 1 + AffiliatePartner.tally_traffic_totals(day0, day1) + AffiliateTrafficTotal.count.should eq(2) + traffic_total_day_before = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0 - 1, user_partner1.affiliate_referral_id) + traffic_total_day_before.visits.should eq(0) + traffic_total_day_before.signups.should eq(0) + traffic_total_day0 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0, user_partner1.affiliate_referral_id) + traffic_total_day0.visits.should eq(0) + traffic_total_day0.signups.should eq(1) + + # add in a visit + visit = FactoryGirl.create(:affiliate_referral_visit, affiliate_partner: user_partner1.affiliate_referral) + + # it won't get seen though because we've moved on + AffiliatePartner.tally_traffic_totals(day1, day1) + AffiliateTrafficTotal.count.should eq(2) + traffic_total_day_before = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0 - 1, user_partner1.affiliate_referral_id) + traffic_total_day_before.visits.should eq(0) + traffic_total_day_before.signups.should eq(0) + traffic_total_day0 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0, user_partner1.affiliate_referral_id) + traffic_total_day0.visits.should eq(0) + traffic_total_day0.signups.should eq(1) + + # manipulate the visit created_at so we can record it + visit.created_at = day1 + visit.save! + day2 = day1 + 1 + AffiliatePartner.tally_traffic_totals(day1, day2) + AffiliateTrafficTotal.count.should eq(3) + traffic_total_day_before = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0 - 1, user_partner1.affiliate_referral_id) + traffic_total_day_before.visits.should eq(0) + traffic_total_day_before.signups.should eq(0) + traffic_total_day0 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0, user_partner1.affiliate_referral_id) + traffic_total_day0.visits.should eq(0) + traffic_total_day0.signups.should eq(1) + traffic_total_day1 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day1, user_partner1.affiliate_referral_id) + traffic_total_day1.visits.should eq(1) + traffic_total_day1.signups.should eq(0) + + # now create 2 records on day 2 for visits and signups both, and a partner with their own visit and signup, and do a final check + + user_partner2.touch + + visit2 = FactoryGirl.create(:affiliate_referral_visit, affiliate_partner: user_partner1.affiliate_referral) + visit3 = FactoryGirl.create(:affiliate_referral_visit, affiliate_partner: user_partner1.affiliate_referral) + visit_partner2 = FactoryGirl.create(:affiliate_referral_visit, affiliate_partner: user_partner2.affiliate_referral) + visit2.created_at = day2 + visit3.created_at = day2 + visit_partner2.created_at = day2 + visit2.save! + visit3.save! + visit_partner2.save! + + user2 = FactoryGirl.create(:user, affiliate_referral:user_partner1.affiliate_referral) + user3 = FactoryGirl.create(:user, affiliate_referral:user_partner1.affiliate_referral) + user2.created_at = day2 + user3.created_at = day2 + user_partner2.created_at = day2 + user2.save! + user3.save! + user_partner2.save! + + + day3 = day2 + 1 + AffiliatePartner.tally_traffic_totals(day2, day3) + AffiliateTrafficTotal.count.should eq(5) + traffic_total_day_before = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0 - 1, user_partner1.affiliate_referral_id) + traffic_total_day_before.visits.should eq(0) + traffic_total_day_before.signups.should eq(0) + traffic_total_day0 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day0, user_partner1.affiliate_referral_id) + traffic_total_day0.visits.should eq(0) + traffic_total_day0.signups.should eq(1) + traffic_total_day1 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day1, user_partner1.affiliate_referral_id) + traffic_total_day1.visits.should eq(1) + traffic_total_day1.signups.should eq(0) + traffic_total_day2 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day2, user_partner1.affiliate_referral_id) + traffic_total_day2.visits.should eq(2) + traffic_total_day2.signups.should eq(2) + traffic_total_day2 = AffiliateTrafficTotal.find_by_day_and_affiliate_partner_id(day2, user_partner2.affiliate_referral_id) + traffic_total_day2.visits.should eq(1) + traffic_total_day2.signups.should eq(1) + end + end + + describe "boundary_dates" do + it "1st quarter" do + start_date, end_date = AffiliatePartner.boundary_dates(2015, 0) + start_date.should eq(Date.new(2015, 1, 1)) + end_date.should eq(Date.new(2015, 3, 31)) + end + + it "2nd quarter" do + start_date, end_date = AffiliatePartner.boundary_dates(2015, 1) + start_date.should eq(Date.new(2015, 4, 1)) + end_date.should eq(Date.new(2015, 6, 30)) + end + + it "3rd quarter" do + start_date, end_date = AffiliatePartner.boundary_dates(2015, 2) + start_date.should eq(Date.new(2015, 7, 1)) + end_date.should eq(Date.new(2015, 9, 30)) + end + + it "4th quarter" do + start_date, end_date = AffiliatePartner.boundary_dates(2015, 3) + start_date.should eq(Date.new(2015, 10, 1)) + end_date.should eq(Date.new(2015, 12, 31)) + end + end + + describe "boundary_dates_for_month" do + it "invalid month" do + expect{AffiliatePartner.boundary_dates_for_month(2015, 0)}.to raise_error + end + + it "January" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 1) + start_date.should eq(Date.new(2015, 1, 1)) + end_date.should eq(Date.new(2015, 1, 31)) + end + + it "February" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 2) + start_date.should eq(Date.new(2015, 2, 1)) + end_date.should eq(Date.new(2015, 2, 28)) + end + + it "March" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 3) + start_date.should eq(Date.new(2015, 3, 1)) + end_date.should eq(Date.new(2015, 3, 31)) + end + + it "April" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 4) + start_date.should eq(Date.new(2015, 4, 1)) + end_date.should eq(Date.new(2015, 4, 30)) + end + + it "May" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 5) + start_date.should eq(Date.new(2015, 5, 1)) + end_date.should eq(Date.new(2015, 5, 31)) + end + + it "June" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 6) + start_date.should eq(Date.new(2015, 6, 1)) + end_date.should eq(Date.new(2015, 6, 30)) + end + + it "July" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 7) + start_date.should eq(Date.new(2015, 7, 1)) + end_date.should eq(Date.new(2015, 7, 31)) + end + + it "August" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 8) + start_date.should eq(Date.new(2015, 8, 1)) + end_date.should eq(Date.new(2015, 8, 31)) + end + + it "September" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 9) + start_date.should eq(Date.new(2015, 9, 1)) + end_date.should eq(Date.new(2015, 9, 30)) + end + + it "October" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 10) + start_date.should eq(Date.new(2015, 10, 1)) + end_date.should eq(Date.new(2015, 10, 31)) + end + + it "November" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 11) + start_date.should eq(Date.new(2015, 11, 1)) + end_date.should eq(Date.new(2015, 11, 30)) + end + + it "December" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2015, 12) + start_date.should eq(Date.new(2015, 12, 1)) + end_date.should eq(Date.new(2015, 12, 31)) + end + + it "February in a leap year" do + start_date, end_date = AffiliatePartner.boundary_dates_for_month(2016, 2) + start_date.should eq(Date.new(2016, 2, 1)) + end_date.should eq(Date.new(2016, 2, 29)) + end + end + end + end diff --git a/ruby/spec/jam_ruby/models/affiliate_payment_spec.rb b/ruby/spec/jam_ruby/models/affiliate_payment_spec.rb new file mode 100644 index 000000000..14f9f4c13 --- /dev/null +++ b/ruby/spec/jam_ruby/models/affiliate_payment_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe AffiliatePayment do + + let(:partner) { FactoryGirl.create(:affiliate_partner) } + let(:user_partner) { FactoryGirl.create(:user, affiliate_partner: partner) } + + it "succeeds with no data" do + results, nex = AffiliatePayment.index(user_partner, {}) + results.length.should eq(0) + end + + it "sorts month and quarters correctly" do + monthly1 = FactoryGirl.create(:affiliate_monthly_payment, affiliate_partner: partner, closed: true, due_amount_in_cents: 10, month: 1, year: 2015) + monthly2 = FactoryGirl.create(:affiliate_monthly_payment, affiliate_partner: partner, closed: true, due_amount_in_cents: 20, month: 2, year: 2015) + monthly3 = FactoryGirl.create(:affiliate_monthly_payment, affiliate_partner: partner, closed: true, due_amount_in_cents: 30, month: 3, year: 2015) + monthly4 = FactoryGirl.create(:affiliate_monthly_payment, affiliate_partner: partner, closed: true, due_amount_in_cents: 40, month: 4, year: 2015) + quarterly = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner, closed: true, paid:true, due_amount_in_cents: 50, quarter: 0, year: 2015) + results, nex = AffiliatePayment.index(user_partner, {}) + results.length.should eq(5) + result1 = results[0] + result2 = results[1] + result3 = results[2] + result4 = results[3] + result5 = results[4] + + result1.payment_type.should eq('monthly') + result1.due_amount_in_cents.should eq(10) + result2.payment_type.should eq('monthly') + result2.due_amount_in_cents.should eq(20) + result3.payment_type.should eq('monthly') + result3.due_amount_in_cents.should eq(30) + result4.payment_type.should eq('quarterly') + result4.due_amount_in_cents.should eq(50) + result5.payment_type.should eq('monthly') + result5.due_amount_in_cents.should eq(40) + end + +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/affiliate_referral_visit_spec.rb b/ruby/spec/jam_ruby/models/affiliate_referral_visit_spec.rb new file mode 100644 index 000000000..467127500 --- /dev/null +++ b/ruby/spec/jam_ruby/models/affiliate_referral_visit_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe AffiliateReferralVisit do + + let!(:user) { FactoryGirl.create(:user) } + let(:partner) { FactoryGirl.create(:affiliate_partner) } + let(:valid_track_options) { + { + affiliate_id: partner.id, + visited: false, + remote_ip: '1.2.2.1', + visited_url: '/', + referral_url: 'http://www.youtube.com', + current_user: nil + + } + } + + describe "track" do + it "succeeds" do + visit = AffiliateReferralVisit.track( valid_track_options ) + visit.valid?.should be_true + end + + it "never fails with error" do + visit = AffiliateReferralVisit.track( {}) + visit.valid?.should be_false + + options = valid_track_options + options[:affiliate_id] = 111 + visit = AffiliateReferralVisit.track( options) + visit.valid?.should be_true + + options = valid_track_options + options[:current_user] = user + visit = AffiliateReferralVisit.track( options) + visit.valid?.should be_true + end + end +end diff --git a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb index 1adef7d18..9119bdfd9 100644 --- a/ruby/spec/jam_ruby/models/jam_track_right_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_right_spec.rb @@ -168,7 +168,8 @@ describe JamTrackRight do JamTrackRight.stats.should eq('count' => 1, 'signing_count' => 0, 'redeemed_count' => 0, - 'purchased_count' => 1) + 'purchased_count' => 1, + 'redeemed_and_dl_count' => 0) end it "two" do @@ -178,7 +179,8 @@ describe JamTrackRight do JamTrackRight.stats.should eq('count' => 2, 'signing_count' => 0, 'redeemed_count' => 0, - 'purchased_count' => 2) + 'purchased_count' => 2, + 'redeemed_and_dl_count' => 0) right1.signing_44 = true right1.save! @@ -188,7 +190,8 @@ describe JamTrackRight do JamTrackRight.stats.should eq('count' => 2, 'signing_count' => 2, 'redeemed_count' => 0, - 'purchased_count' => 2) + 'purchased_count' => 2, + 'redeemed_and_dl_count' => 0) right1.redeemed = true right1.save! @@ -196,7 +199,8 @@ describe JamTrackRight do JamTrackRight.stats.should eq('count' => 2, 'signing_count' => 2, 'redeemed_count' => 1, - 'purchased_count' => 1) + 'purchased_count' => 1, + 'redeemed_and_dl_count' => 0) right2.is_test_purchase = true right2.save! @@ -204,7 +208,8 @@ describe JamTrackRight do JamTrackRight.stats.should eq('count' => 1, 'signing_count' => 1, 'redeemed_count' => 1, - 'purchased_count' => 0) + 'purchased_count' => 0, + 'redeemed_and_dl_count' => 0) end end @@ -213,7 +218,9 @@ describe JamTrackRight do let(:other) {FactoryGirl.create(:user)} let(:first_fingerprint) { {all: 'all', running: 'running' } } let(:new_fingerprint) { {all: 'all_2', running: 'running' } } + let(:full_fingerprint) { {all: :all_3, running: :running_3, all_3: { mac: "72:00:02:4C:1E:61", name: "en2", upstate: true }, running_3: { mac: "72:00:02:4C:1E:62", name: "en3", upstate: false } } } let(:remote_ip) {'1.1.1.1'} + let(:remote_ip2) {'2.2.2.2'} let(:jam_track_right) { FactoryGirl.create(:jam_track_right, user: user, redeemed: true, redeemed_and_fingerprinted: false) } let(:jam_track_right_purchased) { FactoryGirl.create(:jam_track_right, user: user, redeemed: false, redeemed_and_fingerprinted: false) } let(:jam_track_right_other) { FactoryGirl.create(:jam_track_right, user: other, redeemed: true, redeemed_and_fingerprinted: false) } @@ -233,16 +240,16 @@ describe JamTrackRight do jam_track_right.redeemed_and_fingerprinted.should be_true - mf = MachineFingerprint.find_by_fingerprint(first_fingerprint[:all]) + mf = MachineFingerprint.find_by_fingerprint('all') mf.user.should eq(user) - mf.fingerprint.should eq(first_fingerprint[:all]) + mf.fingerprint.should eq('all') mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ALL) mf.jam_track_right.should eq(jam_track_right) - mf = MachineFingerprint.find_by_fingerprint(first_fingerprint[:running]) + mf = MachineFingerprint.find_by_fingerprint('running') mf.user.should eq(user) - mf.fingerprint.should eq(first_fingerprint[:running]) + mf.fingerprint.should eq('running') mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ACTIVE) mf.jam_track_right.should eq(jam_track_right) @@ -274,40 +281,80 @@ describe JamTrackRight do end it "protects against re-using fingerprint across users (conflicts on all fp)" do + first_fingerprint2 = first_fingerprint.clone jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil MachineFingerprint.count.should eq(2) - first_fingerprint[:running] = 'running_2' - jam_track_right_other.guard_against_fraud(other, first_fingerprint, remote_ip).should eq("other user has 'all' fingerprint") + first_fingerprint2[:running] = 'running_2' + jam_track_right_other.guard_against_fraud(other, first_fingerprint2, remote_ip).should eq("other user has 'all' fingerprint") - mf = MachineFingerprint.find_by_fingerprint(first_fingerprint[:running]) - mf.user.should eq(other) - mf.fingerprint.should eq(first_fingerprint[:running]) - mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT) + mf = MachineFingerprint.find_by_fingerprint('running') + mf.user.should eq(user) + mf.fingerprint.should eq('running') + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ACTIVE) - mf.jam_track_right.should eq(jam_track_right_other) - MachineFingerprint.count.should eq(3) + mf.jam_track_right.should eq(jam_track_right) + MachineFingerprint.count.should eq(4) end it "protects against re-using fingerprint across users (conflicts on running fp)" do + first_fingerprint2 = first_fingerprint.clone jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil MachineFingerprint.count.should eq(2) - first_fingerprint[:all] = 'all_2' - jam_track_right_other.guard_against_fraud(other, first_fingerprint, remote_ip).should eq("other user has 'running' fingerprint") + first_fingerprint2[:all] = 'all_2' + jam_track_right_other.guard_against_fraud(other, first_fingerprint2, remote_ip).should eq("other user has 'running' fingerprint") - mf = MachineFingerprint.find_by_fingerprint(first_fingerprint[:all]) - mf.user.should eq(other) - mf.fingerprint.should eq(first_fingerprint[:all]) - mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_FRAUD_CONFLICT) + mf = MachineFingerprint.find_by_fingerprint('all') + mf.user.should eq(user) + mf.fingerprint.should eq('all') + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ALL) - mf.jam_track_right.should eq(jam_track_right_other) - MachineFingerprint.count.should eq(3) + mf.jam_track_right.should eq(jam_track_right) + MachineFingerprint.count.should eq(4) + + FraudAlert.count.should eq(1) + fraud = FraudAlert.first + fraud.user.should eq(other) + fraud.machine_fingerprint.should eq(MachineFingerprint.where(fingerprint:'running').where(user_id:other.id).first) + end + + it "ignores whitelisted fingerprint" do + whitelist = FingerprintWhitelist.new + whitelist.fingerprint = first_fingerprint[:running] + whitelist.save! + + first_fingerprint2 = first_fingerprint.clone + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + first_fingerprint2[:all] = 'all_2' + jam_track_right_other.guard_against_fraud(other, first_fingerprint2, remote_ip).should be_nil + + FraudAlert.count.should eq(0) + end + + it "does not conflict if same mac, but different IP address" do + first_fingerprint2 = first_fingerprint.clone + jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + first_fingerprint2[:all] = 'all_2' + jam_track_right_other.guard_against_fraud(other, first_fingerprint2, remote_ip2).should eq(nil) + + mf = MachineFingerprint.find_by_fingerprint('all') + mf.user.should eq(user) + mf.fingerprint.should eq('all') + mf.when_taken.should eq(MachineFingerprint::TAKEN_ON_SUCCESSFUL_DOWNLOAD) + mf.print_type.should eq(MachineFingerprint::PRINT_TYPE_ALL) + mf.jam_track_right.should eq(jam_track_right) + MachineFingerprint.count.should eq(4) + + FraudAlert.count.should eq(0) end # if you try to buy a regular jamtrack with a fingerprint belonging to another user? so what. you paid for it it "allows re-use of fingerprint if jamtrack is a normal purchase" do + first_fingerprint2 = first_fingerprint.clone jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil MachineFingerprint.count.should eq(2) - jam_track_right_other_purchased.guard_against_fraud(other, first_fingerprint, remote_ip).should be_nil + jam_track_right_other_purchased.guard_against_fraud(other, first_fingerprint2, remote_ip).should be_nil MachineFingerprint.count.should eq(2) end @@ -319,13 +366,22 @@ describe JamTrackRight do it "let's you download a free jamtrack if you have a second but undownloaded free one" do right1 = FactoryGirl.create(:jam_track_right, user: user, redeemed: true, redeemed_and_fingerprinted: false) + first_fingerprint2 = first_fingerprint.clone jam_track_right.guard_against_fraud(user, first_fingerprint, remote_ip).should be_nil MachineFingerprint.count.should eq(2) - right1.guard_against_fraud(user, first_fingerprint, remote_ip).should eq('already redeemed another') + right1.guard_against_fraud(user, first_fingerprint2, remote_ip).should eq('already redeemed another') MachineFingerprint.count.should eq(2) end + it "creates metadata" do + right1 = FactoryGirl.create(:jam_track_right, user: user, redeemed: true, redeemed_and_fingerprinted: false) + + jam_track_right.guard_against_fraud(user, full_fingerprint, remote_ip).should be_nil + MachineFingerprint.count.should eq(2) + MachineExtra.count.should eq(2) + + end end end diff --git a/ruby/spec/jam_ruby/models/sale_spec.rb b/ruby/spec/jam_ruby/models/sale_spec.rb index 47dda7213..a0d4acf48 100644 --- a/ruby/spec/jam_ruby/models/sale_spec.rb +++ b/ruby/spec/jam_ruby/models/sale_spec.rb @@ -132,6 +132,69 @@ describe Sale do user.has_redeemable_jamtrack.should be_false end + it "for a free jam track with an affiliate association" do + partner = FactoryGirl.create(:affiliate_partner) + user.affiliate_referral = partner + user.save! + + shopping_cart = ShoppingCart.create user, jamtrack, 1, true + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart]) + + user.reload + user.sales.length.should eq(1) + + sales.should eq(user.sales) + sale = sales[0] + sale.recurly_invoice_id.should be_nil + + sale.recurly_subtotal_in_cents.should eq(0) + sale.recurly_tax_in_cents.should eq(0) + sale.recurly_total_in_cents.should eq(0) + sale.recurly_currency.should eq('USD') + sale.order_total.should eq(0) + sale.sale_line_items.length.should == 1 + sale_line_item = sale.sale_line_items[0] + sale_line_item.recurly_tax_in_cents.should eq(0) + sale_line_item.recurly_total_in_cents.should eq(0) + sale_line_item.recurly_currency.should eq('USD') + sale_line_item.recurly_discount_in_cents.should eq(0) + sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE) + sale_line_item.unit_price.should eq(jamtrack.price) + sale_line_item.quantity.should eq(1) + sale_line_item.free.should eq(1) + sale_line_item.sales_tax.should be_nil + sale_line_item.shipping_handling.should eq(0) + sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code) + sale_line_item.product_id.should eq(jamtrack.id) + sale_line_item.recurly_subscription_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should be_nil + sale_line_item.recurly_adjustment_credit_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid) + sale_line_item.recurly_adjustment_credit_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_credit_uuid) + sale_line_item.affiliate_referral.should eq(partner) + sale_line_item.affiliate_referral_fee_in_cents.should eq(0) + + # verify subscription is in Recurly + recurly_account = client.get_account(user) + adjustments = recurly_account.adjustments + adjustments.should_not be_nil + adjustments.should have(0).items + + invoices = recurly_account.invoices + invoices.should have(0).items + + + # verify jam_track_rights data + user.jam_track_rights.should_not be_nil + user.jam_track_rights.should have(1).items + user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id) + user.jam_track_rights.last.redeemed.should be_true + user.has_redeemable_jamtrack.should be_false + end + + it "for a normally priced jam track" do user.has_redeemable_jamtrack = false user.save! @@ -201,6 +264,85 @@ describe Sale do user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id) user.jam_track_rights.last.redeemed.should be_false user.has_redeemable_jamtrack.should be_false + + sale_line_item.affiliate_referral.should be_nil + sale_line_item.affiliate_referral_fee_in_cents.should be_nil + end + + + it "for a normally priced jam track with an affiliate association" do + user.has_redeemable_jamtrack = false + partner = FactoryGirl.create(:affiliate_partner) + user.affiliate_referral = partner + user.save! + shopping_cart = ShoppingCart.create user, jamtrack, 1, false + client.find_or_create_account(user, billing_info) + + sales = Sale.place_order(user, [shopping_cart]) + + user.reload + user.sales.length.should eq(1) + + sales.should eq(user.sales) + sale = sales[0] + sale.recurly_invoice_id.should_not be_nil + + sale.recurly_subtotal_in_cents.should eq(jam_track_price_in_cents) + sale.recurly_tax_in_cents.should eq(0) + sale.recurly_total_in_cents.should eq(jam_track_price_in_cents) + sale.recurly_currency.should eq('USD') + + sale.order_total.should eq(jamtrack.price) + sale.sale_line_items.length.should == 1 + sale_line_item = sale.sale_line_items[0] + # validate we are storing pricing info from recurly + sale_line_item.recurly_tax_in_cents.should eq(0) + sale_line_item.recurly_total_in_cents.should eq(jam_track_price_in_cents) + sale_line_item.recurly_currency.should eq('USD') + sale_line_item.recurly_discount_in_cents.should eq(0) + sale_line_item.product_type.should eq(JamTrack::PRODUCT_TYPE) + sale_line_item.unit_price.should eq(jamtrack.price) + sale_line_item.quantity.should eq(1) + sale_line_item.free.should eq(0) + sale_line_item.sales_tax.should be_nil + sale_line_item.shipping_handling.should eq(0) + sale_line_item.recurly_plan_code.should eq(jamtrack.plan_code) + sale_line_item.product_id.should eq(jamtrack.id) + sale_line_item.recurly_subscription_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should_not be_nil + sale_line_item.recurly_adjustment_credit_uuid.should be_nil + sale_line_item.recurly_adjustment_uuid.should eq(user.jam_track_rights.last.recurly_adjustment_uuid) + sale_line_item.affiliate_referral.should eq(partner) + sale_line_item.affiliate_referral_fee_in_cents.should eq(20) + + # verify subscription is in Recurly + recurly_account = client.get_account(user) + adjustments = recurly_account.adjustments + adjustments.should_not be_nil + adjustments.should have(1).items + purchase= adjustments[0] + purchase.unit_amount_in_cents.should eq((jamtrack.price * 100).to_i) + purchase.accounting_code.should eq(ShoppingCart::PURCHASE_NORMAL) + purchase.description.should eq("JamTrack: " + jamtrack.name) + purchase.state.should eq('invoiced') + purchase.uuid.should eq(sale_line_item.recurly_adjustment_uuid) + + invoices = recurly_account.invoices + invoices.should have(1).items + invoice = invoices[0] + invoice.uuid.should eq(sale.recurly_invoice_id) + invoice.line_items.should have(1).items # should have single adjustment associated + invoice.line_items[0].should eq(purchase) + invoice.subtotal_in_cents.should eq((jamtrack.price * 100).to_i) + invoice.total_in_cents.should eq((jamtrack.price * 100).to_i) + invoice.state.should eq('collected') + + # verify jam_track_rights data + user.jam_track_rights.should_not be_nil + user.jam_track_rights.should have(1).items + user.jam_track_rights.last.jam_track.id.should eq(jamtrack.id) + user.jam_track_rights.last.redeemed.should be_false + user.has_redeemable_jamtrack.should be_false end it "for a jamtrack already owned" do diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 5e9380eac..8d16dffbb 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -198,6 +198,14 @@ def app_config true end + def expire_fingerprint_days + 14 + end + + def found_conflict_count + 1 + end + private def audiomixer_workspace_path diff --git a/web/Gemfile b/web/Gemfile index c06ceac7d..c5f2e3562 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -18,6 +18,7 @@ else gem 'jam_websockets', "0.1.#{ENV["BUILD_NUMBER"]}" ENV['NOKOGIRI_USE_SYSTEM_LIBRARIES'] ||= "true" end + gem 'oj', '2.10.2' gem 'builder' gem 'rails', '~>3.2.11' diff --git a/web/README.md b/web/README.md index 975118c93..393628cf1 100644 --- a/web/README.md +++ b/web/README.md @@ -3,3 +3,4 @@ Jasmine Javascript Unit Tests Open browser to localhost:3000/teaspoon + diff --git a/web/app/assets/fonts/Raleway/Raleway-Bold.ttf b/web/app/assets/fonts/Raleway/Raleway-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..adc44af0b26250094fc8936f1778910969a0ab33 GIT binary patch literal 62224 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 literal 0 HcmV?d00001 diff --git a/web/app/assets/fonts/Raleway/Raleway-Bold.woff b/web/app/assets/fonts/Raleway/Raleway-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..f4e18af9ebbf2fdaccbff652095b78dcd4c9278d GIT binary patch literal 31568 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 literal 0 HcmV?d00001 diff --git a/web/app/assets/fonts/Raleway/Raleway-Bold.woff2 b/web/app/assets/fonts/Raleway/Raleway-Bold.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..a7f14579160bf176b070887de8b9c006c2d57b87 GIT binary patch literal 22592 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 af<)CCa~ZTlKykp&zWpDy%=pXz literal 0 HcmV?d00001 diff --git a/web/app/assets/fonts/Raleway/Raleway-Heavy.ttf b/web/app/assets/fonts/Raleway/Raleway-Heavy.ttf new file mode 100644 index 0000000000000000000000000000000000000000..b32bcf64cfeb7d2407881d0367f936292a980c44 GIT binary patch literal 63056 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�%)*cp|h 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

    Vs+{DX`UZ|SIm*!eIEIc7 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%0D44!S)fm=1 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 N7*;_XPXhc`@$Yk|8lV6G literal 0 HcmV?d00001 diff --git a/web/app/assets/fonts/Raleway/Raleway-Light.woff b/web/app/assets/fonts/Raleway/Raleway-Light.woff new file mode 100644 index 0000000000000000000000000000000000000000..85f92d954d3934333e0aec42d5de67d1db301da6 GIT binary patch literal 31592 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

    nclP+UiD} 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 literal 0 HcmV?d00001 diff --git a/web/app/assets/fonts/Raleway/Raleway-Light.woff2 b/web/app/assets/fonts/Raleway/Raleway-Light.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..8e8ac1f40f9638fae522548eabf5d956343ade53 GIT binary patch literal 22628 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 literal 0 HcmV?d00001 diff --git a/web/app/assets/fonts/Raleway/Raleway.eot b/web/app/assets/fonts/Raleway/Raleway.eot new file mode 100644 index 0000000000000000000000000000000000000000..fc7ce0746b0385fea594471b97e0d407e7f3ee6f GIT binary patch literal 30373 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 literal 0 HcmV?d00001 diff --git a/web/app/assets/fonts/Raleway/Raleway.svg b/web/app/assets/fonts/Raleway/Raleway.svg new file mode 100644 index 000000000..92afeb5d6 --- /dev/null +++ b/web/app/assets/fonts/Raleway/Raleway.svg @@ -0,0 +1,2168 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/app/assets/fonts/Raleway/Raleway.ttf b/web/app/assets/fonts/Raleway/Raleway.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3b354c87e94c41924cb93bb1888fa5e9f4add14d GIT binary patch literal 63796 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 literal 0 HcmV?d00001 diff --git a/web/app/assets/fonts/Raleway/Raleway.woff b/web/app/assets/fonts/Raleway/Raleway.woff new file mode 100644 index 0000000000000000000000000000000000000000..02b0dd8f5a252e6bf06b5775d0b24cd1e6300e42 GIT binary patch literal 31836 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 z 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

    s z&P`xXVi8)qrVNEmU0_mf+f(XOwVl8m`iZufpT-X0$wFa^9k9= z5VPogZqw99edV`ZU)V`kf_idQwU9DnV5sg#or6A*+_wv(c)zLS zN9hma7~V8J=*AIyK)a}5ae9=`R==HyXU5!&kN5x@U-(; zrwiI2VUB|Q%Ua5+BJn&ygW`|-sW-G7`OVauTCwbWB*TDwv7AWxzC1NiNKcFX$v1ma zO{{dnFJnoCBuQa?ZQunKscAS1hJ_=LxH?fYH=DMga#3U^)*ef?`&AP7m$(o8IAptl zyglIprW~2ShxdeQ_}pZV{QpP8t5cDbr!Q6{C4HNbk|-dXqmCw2G)!@*HS}~s|JYJf zp&?fq7*Vd!DGOMDjaDoVg#-^UNfxgeOGn|h1W*3;zr7312{dz^78OV(R*biz#&keN{;F(G;BY~cmbt1{r01t$4w5Zw z%IJl;7yI1h-B8ljZ+ZlcyytK?fzY+O{o~*nX=1ie6^pMuHF?HZjyQ`2zI-1rr^-kN z@9)eEkI8uvy@!I6C~*1$E^6(IkQF3uQCMA4e7}BS^9Giv4lmJc%G#R6(7y$*>z<8A zG-js3p=*MQg4w}47XC*>EhrNvBs4w@1hHFuq+bAd4}c1`ahm$I>@mUnKe6=we+lFs zzoSc5QmUMlIfBM>!h*30JG1lJq!x3#C?`iU#$gqX&B|+&SlD)`;Y^no%5rwei%S(V z(lRyE%L7v+lVROuSjGV(RDg-dEXJ^Q^HDLKJlmRyUeEH^aroyWlPi+Gpi3meM zUPcrs>TKi!DV`mys1ZDbEWf*WgbagOTRk~bps^?A3dF=bwLO!c z(L!x;1eNIj)Wa&R^!4inr>49~H$eBjT|4aRj&x8Fz#qR+SC#y}9BxfB_Y^CO701gj zs|&vpo2M#=T&eqG1ZRC%%2ZjPNY>?XPfeikDUgyfO%^DWcey>(1d6yk9-^g#pOYzW zvs;C2Ilc~|&5AZVOVn<$2-~tRA6A|sZU*rOEmO+Ec;$vnrpj2RDAU6hzu;5j7(0rz z1SU>@V=9K`WCR-$0>}hCyoeYn%jFpDlp?8|T&%Tcu_VSa;H(yyq-J+maMZ$|F!}pR zbuYRrSgTsUqAsKa!ve_P(M|>n=Tulo*Mt+v|C4{zP=>FT!(zk9goKmxQ3*ZS4}tXm z2ZJ(S&CONkssO**|9FqGTbcS(f2M%BD|%)g*1#bf1pB54j$uOK_w zNi)V0mvyZA#6@%H##bH$W1N0HUeZl$G&iiTB>kx%6VBJqYj zwwK}l+gpf1*YE4cVOQ;_$K#9E%xN{e)dlNVC*n`F`GAloXpaE!*t$h4>2bL|d$!yM|v>F?Jfk%IR7EB@v*13f-ru6e~OE`bF; z+5_n3x!rie_TJyIrdnBTCTwo|t`{@l=7lHeGy=2ELK1QeiN~^&F2J{9VbwuLf_=UEf>(t^~_5QGOtubo=!X zC*FLa$OBzIbdx*&x=c;bLS!bb7^A?PLXi?9z{|fGLO3`ZV8e14CIsp@8549QFT)X+ zR&5v4T~=u+_S~4OuY0~Z^#}Eb>^(oi+0d`M)cdFHpQ8TSca!qvsoXbh-_kFAk+9dB z`q-zuDcD_wkv{eQDf?F$k(Kkp<*%>au<&6(oTKrnaZ?fIc{<=aVEtZe(sA!rL()Z( zAwrO$+lw`|mI9@c2G=EZUi&GR8)tzeQT@7B5aL9n8SOhx2g8qhT6x8}(Z7)vl*SI_ zKoeq@y@OEzB6<0?@{lezJ~hz4FRH$xr_sMsZz(wU#fvvZBIh-r6!#Vved+KY^ePxX zL%=0LH~g^c59tp&H+%~R7q2xt$7n^V06ABYsluC&Bu>x*O!&FQg1%>rzlsW;8>X=54OqSuQmhXY>Gio^?iFrGh#_t zBaMbLLX21%&2&LfiKL#VwYF6&T&dS+^yH(Hc!#FSx`n4zqSO7SvBjqwWu5#(UE=xt zV=CdXe!)wr)@PsjhxYOg&0|K6APU>50hyc@0PF>9g<@g=J1roWU0zw%$7l}L!2k$! zIzUw5oE$dJr7#g^%}kt2$k&SPmiqufhAyOSx6BSS)yBMC$sSPeZ9o0vq&By z?VEB?JBJVmq;G5?32U=q*(9#ys>IASTT*PmJUCHOv%d4TF1JDC`ytv>^q?D6)((>cc8~49I1iX-+l=|;*Ozpqe zH(T>P#b|HLV{fFf%y{1b+)gfgTc**-{2YUW5|GGAfv7pbqGp-MQ7A`TOei0!Oms^E zPr2^jl=~$U$rqct$4z}Fv)t9l9dfmNXCcv)OwQWMCzxt%h>6Vs(D;YD#L?`~vvb7o zVrr47XurbE@li^Ul>(3HFI5gpoA25mekZ5sqR#EbQstQb@(7TX)Dn&_S+QSKL@k0B zhacEU$edb&$CpgaA`r?)HP=|pqskt`1$__UiJ$(}L18Z{_@_R8$-PpRQdcISyuEZq zp1_H{kG`wXK>=AKXI4+JHjGngELIhhYQ`x#r)j5ko<@@b8l_3;Qg+SO&Yq;4^zy)s zPl%LeagrURBB_iTFxvfWNpiXNPI+<{wn2S94sAk*y+ zG7i5jMgXMcD~=f%?!VPvyAM!F^{H`aS%%90>h`{(yCkYPv7TrZFv@)~b5I}Wzo96O zjZ}}c>iDJK>gVdm(vj>MuNhAnuO832>&k5rpC!2;`)sF^;{GVO3Tao>$6g5^F9w;+ z;zLA~Tq~8y0Y3`KvfKtu1Icv~2r9FB03cG|x!GqEN5<=g_kLxBdQrRMUax@7)-Lq< zgrh80{wwfhw+mBP`*0Bo5n3pBU9H=ps@gJYt7Pvb<`O0=ls0W&^_R%xX8Ph+d;DaH z&LQF_YRcAC9yh>yZ3OxYiv`Ni1Za1{Po?9ynCo;23V!YV2s*}z;IAgxYQ3rr2Wv~o z0;;Lk21_EYMa{eWx7FlA$JTUUaTC*iC*_Bix*YhIzj$uZ=p$-vNT%MWSZs^4p~&c1 zR}MDm@xsGr8?BJy2qpnUKEVwG;%uSq*Pp;~lgpo`7ByFHznJ0a(XU+3Zr<;CDjO9pg@Y4d%eIE( z$PuI3ry&qqmqH+451d9qQar6y05ZMmnh%~h<|-W3YLnS^0z2Df?ZsFX3b)(aCIW%! z#egH1Ba`{0UV>$%1$H!Dk*x9ybRJRw;4@LK@5UqsUc3PF=T79%yUA!KKt^GlM&AoV zlqX>05*>##uo73JX5hIqQ*4+&dba`QoRmzI3gb?*D)D)BDFkJfDDUsT|LHWvQ+O`a zIJ|*@PejM0?bYkN{Pw%&f6g3O^beGyY*EK+)SYgoU$^s@0#a4fDM89Vwa_h%C79Y{Xc zg2XH%vTDZkG7kOj<7*4FGKz)%^!fikE8n)RMG#nG)abXx6}Kg$rcR*RZ6+c4$MOF^ zeV^Gm5AvQZeHNMSZD2w!y9W|QAfn+mPP1_%Eh^gbg%2v^9^RDnY&ou@TJ5R7 z;+idAJ!#jQhh)js52A$Shhw#!+OhD6{o>w(Mi_5LDC;7% zD;deqf3Wyf_63z`n{k`Ucj37_!pw-3C&tZPal$?b9t^gxI6kLy7CHulzdxjT~EB4lU-OnuDe|yT9VcM8!&xmZHAy8j9DO9 z&~NFVCzywO&>+aV;&VOuJ&7ynPVFz;k}clA%imHU-Y(-&jMO?~>M)>2GNyjvOOk8h zssMuw?fc&l?95m2f3#0_vG9D=m>2+HOKtdNlsAwH^Xnr*2yXW;Wnv?LKNkN?>}x?n z3NOK?X{AZJ;+|=c5e|RGM^TzU_5hG6w#(UbOBVV9Fx)KL7*1VB z(VBN1U@fg$*v2eLOJw8R77B1o^e$Hojw%2#ovpOQB?F{nRXEfyj9hS;vYL_r^g_U- zaNa)Y-c4rjg$S{~9Tgu8v`ngRCY*%ihu+>-IR@fEkx1*-D6dxF$)~4d(~bMLGFXBEP5#e4?Ws$sa}U1IE(&|ne*1F-|C#MY@g<3_2M1OSOIupq?HjIqp7+i+BF zhTUfq;E{_?gT2hab2&9t8b@4DVZ%vnBQcIxlu{F7A9N>NYqCvHm6?k|!bRYRDdJWi zW{?^s&-849*g z1RD_`g2WyG6^Yov3?sn4m%9W+dg-P0ih*3AZ3MnJb8X^a$C@*f6jUXysL-)*BC_xi zf)8k^ozK4m5PimpTTNA90kV)2JY#IRXzDn5SnjsRjyjH9$awkO%A? z+L&DwqOBpM*yjS{qv1IfuJ;(BCa>BEU_8U%VGanW3cB2?O^Fd$p}r$Tb~VZ}E$uAE zB?~hlwV+)*=_qU24A55U5E>jL*_TW((P>pWxQR;0CFKt#!gq=8ZE7sV_7tw(RB<5z zAPL`4r=MrdgmDal@yx^769OD*6N|?saTM|ZPB2fXDBh5nPe3970%j`nGi^djN^CP# zdp&=P2f*ERUaqI(VZWa05}RZH0g)LnhRspTc>jKJQ9$;qh=HVqs4Zq+tr&V1S_Z4euZ6C1e>cA1R7?Ffgd z3#yEcr4;~~1aEz4hhtVfKcaUYpbj2jsTKWoC@>JlRl`NKO+u+;*Ko%FWJ-{sW0C0Z zi+L?3-weh9!6gMUIxjasQm!)*z%m3-0~2W~N;ZLHL4iDDP1ix$fB-5AOL8p1TWG!) zX+_^umld2WgD?^$ds0OoLCEqDwV}6?%}&un*$JosV{&p6`r1;<5^4gDWJ#~7xJCx8 z1oDyw0%3(D669@_Rg7GA0nnmL?*KqRFC+-Gs(uoI8Dm!iC(VLxRB97}Dd0glK=Ry# zWF&Ehgp%tr1mq|rJErUH17Uzz04X;eh?9ibTvx=GP8;>{q0Eyg@NKj1yM05BF4&50 zYq5n1Zpm|Na#S&9`!W(CdiWKS>gS0tTJTWsXeur8B{3X}8cRwl&!Bh6g%qX-M~R0i z4vm5pSHS}Z3~W?5kO*doi0#6BT><7-)u}$gz23V#b7Dcy(NiLejeOOVBR6G=Styc+ z&5=yjQA&~Jl;*Zi3B;D&=tulH5q+UixlQjY9)a57)p$?L=PG0d;((sQ(-3`3Rd`m-=m zG>0Q^at0w}*IF2{t;Dr5mwy5bVZW0X>dNz`5z~&&XwR}McLc5l4%oeGYZx$GFte{Y zWmCg=QhD>bEG&k3BTUD*MG2Kv1DGuUD!ap$Jw6!xnu4Y$#jGqah@W4$~Hz*Gm?ySnKC z$#rNsDV$bIQ0;kCwGl*mh-3jZHox{=SRlgU71U`Yu_s7i5!^{PjwC(syEmgS!B2YQ zlwHWd22mQM%C@lXSV7RXnFCl!U;^f!2A?(%={gA4S5D?;iJz+AgeST3G+CKQHaM|Q zG*FDv;aeAJzK($s>y_D3;F6c_Gx&u*))~XPdnJ=WWxF_cZKK+a-&mB{B*DDV4oE6x zbTT-mrQs^@%s$=cds{Lm)m%N=iad_#zC`C=fpRhIovRrpmKOi`o?IxT_BBr)hWH z$CRFlAD*zKM~2-I;Ug?XvN(W9#zgE#z~i1yIwL@AJqy5`RJVs%=Fc0!y+`HD%=yfJ zjwO7vUwt{Zc4xwQ`^1{*pKBZ`=h@V>W64B$A#%^I@ht#H>@Y}$0%j@i{Fh(+rMjBN z;nh=K_V^Cv`rSwD*RP*X#@Mg_tj6MLY+h-CMe9i)-74MVI`3v$N(TDMy_J?XqV&*2KaDP0X&Z}$6@UnfG$D!|5ea)}ucmMI2<;<2S zJv;vLvX}2S$AeW|Wmr^Q*B-it7(}|e9lE>bEb=^r*Ns-o{1-E|58o0c3f%IddcrCUo_IRQXOMkD+SA940KXF^-i!3&Ij~%Si@+D~2Z+a)PUm?-rb5WsX$Hhfkp26S%&Aka< z?a|@#`^T@RXMVXO+NpHGazgjt&M3sHI_z4l6X(C`U6l7Y>jpkJ)4_&i#XT6=15Odd zjGwCu)pU+Aw&Mu7uW5cf4MSBy&v(%@r2JuiiF7}3cn&KL%{#Vx`MCIq@+S7iS7=H{ zmDYZo^P)J7T`VTw2!%9bR?p@dtHh>tK0YSfd zfWD|*91VYDw-HZ4LcMAYHVvWu7O&^#AUEu+Q&VO=38LmA<@Jl(^d;gHi{Ky)ZA&mE zPim5J1RuwxFNcHEMEIOjuk#%bqqY`5vcG9f=tD#)KPk{FUfu6TB@g8NtmvLflf1-4 zI>Y-o(5Yb4_hm>IBl-Du*1Oj)k+O-?^XcHm)x)W%lsv6i7^TBG(1B0XZs8%2-1S)D zBP;+%6(bnX9VNJxSehd(E-tQxw4dQ*@Nyh7o+gcCYg=G;+K)ja-ikV5E%Aa?OJgT< zQL+Lal?D#ceb{^)wJ}M~-(g-FzZQ&>weB3>o7grF8Gwmk+sa9IJ#fF`sn3_`@-v=_ z6ghp%fxYy>$jQt>RRC-5 zK53S=5Ffx(*09}0&=Cq|G9;wE?BsSKlfj_vETt)&-t0BKk;{8t_+q`^+6LLM6uQUV zqGI32NhaM&M5TKDNt;h{Xe(%g>wD@KhD4G3sJoCD0;vs}Vw;dlmsM=BngYq)D+z3V z2@K!k+e71k42@|q1vN>Z76k=Sip*;NBOfxEf(E}dQG*arcSTrE~d?l zWahYa!p49$D`SkFrR^0Vjm>XKx3p<%G+*V|^uFh_`#u3C;@;iVtDcNaUO;V{f1f_; z!Z$K7P`bz3q{|S1fQW~M5fS%H_q}OZ{9H>ZgT>^ekxPu!i69?Y_?JJ>2v8b<=-rf zVyyFyD1_&8RNVnzo=&7w)Iut}NTw??EMb(;PsAYqL6NInyC>zsMVU5w;TEr@=E4<~ zX9=Jrv;@(}-@BkIi6ZfOoJ#ynKE7uR>GeInOuqe7YI5NPL9#yoXioDm+R8(`*LWi75RxJtf=}#W?vrPy98% znLYHr7@CVyy{Xic;he9H@Cop^B#l}*-WvSC{9dNh8NRmBuDRo`I}8tj*OLt3zhnsM zPe-Zd9aqpfEVL`H5wa9GVI#Q)jXA#?g~CDHrr)TdqMx&wyva_=;)xz_Af~`1O40Lm zKA2qQTJtrDTy@NYON_jOPy*6Y5mi4548cZ8sb5_(x0YAajRT(-UIjld(( zYXtM`YYTkhzR1PkksXz4k-emp-W@JdxG5eRXwcFE-Yyy+HP(AdO(P+cqXb_?|@}t6~M!_G6=OA8l0Bwa@IF!yul5R4pP8&ye)l ztY^gu8(OS7T^RS4W{+?9!Y28EbKj|G{r3M z^~O|-o0kvH^qy6U-?LzMM$K$ks}2cxSIC`|<>0bX?MJYmbiXhqNqzm?-VSBf^Gq_3A-rM;)36&see4 zY_TAV{QE^tlo^0SCf^pdZ3Sr$GOBKdD@jNWPb^#Ilf|<}4(ikrMQw6^d#ONIYdQfw zE=>atuUv~Xa{6}fMg&CyyiW(h33fyYkh4jHV}CMwGkS~$PsjLtS?1Y|XT?S3u^5gP z%!Ba$fJjZg=T$w%IR-1-s}gT_qeB<`}goaWL?i+)@2=-!C4nCSqs&e?uPcC zDpxj%c)SncKP++GBRnw#Dz&n)LR&PE2@e?qR}-#K*TpUb$-lNHK15!Jis0$v3F_#c z22)@Oaz9O@6VTVSFk`l`2sTg`w?v*YlNz)M6CcPq{bJ_|eQ#h<+c!k*?a~!!S2yKJ5 z85$1NN(ts_Wx=Z9YuS#31N)Wv^~kt4H8mUsvyrxzMdj3Vy<3)h7{8$2d5WnNHS~OL7?Spp zYj*r3FOGZZI_wA^=@s3RyYAGmB)p_9yEuY096Nv}vrjm=59acELF9Sap~_3~Zf%io zl@W^lpI%C3oBsf?)uZxk&NAQhC&?yFk^z_1w`MEq+M64NqloW*F zTLVbW;H#VxCb4J$BL-}Z{RZ)X|KKV($%l&TW;XSXwz77$<7OLE~&0USM_Q|*7`vOUK z?o<96=w=RG`VjG>xa~>Z&AWB82r3}r6s$MZrV*Jbxt~Dp*fp(F{^DlT-dVnDgWg1%_#~~Nlb{4AqSF<=wHKSK6oP`g!<10B|R9pl|5T~y$R-zjx zpO0&9yH}zhxqHW@ zO(=a5N<`o#`QCLJ)5> zNYmZqZn%Os3Em_RJ3U*3)Pt6(3Q?cRs;CHm<)D|Zg!i8^xFgCL!7DG~0EI5j`Svb?~x2*ed7l@LQs z+xGM<*~dsHRR`sx+7;98!G7T3h#;$QuVpreF++I>!|72rSvSlm67rV}-}U(q5pw63 zDKXyb60qtOEF3Gg2=!>9y*h70okDKcH%)ibcna;Mnb^raxQnITCh{WS+|qIKRCmb> zRUe1P@**I{+odC>)1XcuYCP{>Ug@$kTrm_~2EvqlPu5FFTkw7ZNpB;eZJsz9wIeQy!}kp42(dc*3T z=qab-@sj7Vdu1}?j+cW?N`z5b^+2TNa49VUP&Nfm(BS26Jh(d+hb;00{|Ddvj z??MX1V(rW{;{1-nm-E59C#KLS1jhX6X?N_nYDVknvt&dhb`?@WqqIEpP1q6p+JB&D z1eY}ji8Cs!hr1~H-rHc`=AGaREMS`C_cIEYb@$cnZML9LH4^G()Gr#_V-gPx$0wn9 z0bl7kythaOc{lhC2H|*mWhH>0WECbm6M}{ZHjfwvl;4(9YZPlYmWR2>bx<*hBp%ui zlMP*n4=Y;ANhJm+lTHC*HMec3YRUa)I7HV`hpW_#tf!(A%79xroKCtN{g2j61h7ab z#i3OOLS|wc&qbT&m6buStvff%EY3zB9~|edt=R%rM4%3; zJUHeq8bl4u)>_SFfZsOo+(~g&YL__z>5rv!z~YpG6U&#$F3h%NY!+i*B{Yh@8&c}v z?1yz+c7AK6VECZf*PTE=W-d;efYTXuS5>%-Jey~kuL<))@z&idPJYXkB1RI;Qwh}2 z*Z7kBQB)g!xuB~Rmfwp6f6*cT6tfQgtTQ!YL}4#vY>K=w1}Zq_?Hm$$a-=mk3zu8p zOo%UmQM*BDB51aOUVG6BPgaZi| zZ!fRK%7>){j(;P5&hC=}dxmG1)CwuIe@0`}|3dzx>*|y5Pt*Nd@2`2m^*?CR``zI4 zUs~J0+5B?W{)5tAk^h|6|8u9G1pnOmr&YoIm+to*^yfrLi^IzAA$0gBU-V!5;wvqA z;6DeV;Qu(baU3$z(j~ugrvH1t_r~@$7s+JoozEgy!|%?fN*_BuOT%7rXNT4PxXwFlP8wzGW}Bz z$^YoOV*&wyAdcams~`?!KuXjo&7VBaqN0SUzmSgni3fp_t$zw-i7Nh07%(08Q~Z~{ zd7K@YGXTK92j~^x-`h}>{)NG4d1tO+Ld}2p{`Z4SANbVY+?&x%#`|$r=?T8N4w$uR zbfU{T3!(X8HGE}lm3Xdtnt&U1h3pA@Bwd1t)xZBE`U4A50_XpYzHWj45&}^E_ma&` z{(UjKUdr}0%?tE@_BuR3>c8q2AgRwyM(aiPw(7$1(_7r=S(s+5ja%3vTGeT$9a(4fPUI?Ta#b z-MYEDJ|3bY)hcXXoH(>0X&AOORTs?K zmDOQc_WhP{xlKkDM`|s#LDL!b@Orn{#0h#-Ji1#DkKq<|5qN z*;Ms<*_M{esXw>bJkWmImo;#ulZ!j?_%BX(V7=Z`M4Gw`NsX0|%P+CBF1iS>FqSvJ#rR0dG$t%-yvS z)#{oCV}`ne*F%=yI2^(piBg~d-VNtZk(*wD0f*~;kc|c9@W=?xbv)5Jy4zEtul_AA z^jWsTpTB-_6bCX!n$!wEd$5T1o8BXGX_uV1Q@NnpN!>9GsHz_mR(~>!xzB2EKE?;h z$%c@OWmkR4>4)%DA;O3DIs(|mX>k?v0tE!yiYjLw&VML_aFHHf)>S{El+!mTc?sQ6 s-1Z!$etx*#Gcnb}+_tgLF16}OzG@M$cN&I;YaTvWkUf9+Xom~ literal 0 HcmV?d00001 diff --git a/web/app/assets/images/content/agree_button.png b/web/app/assets/images/content/agree_button.png new file mode 100644 index 0000000000000000000000000000000000000000..b30ecf4805d64b4bd71f51c91ff6eb276586e9fa GIT binary patch literal 13570 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 IM6N<$f*IJVk^lez literal 0 HcmV?d00001 diff --git a/web/app/assets/images/content/disagree_button.png b/web/app/assets/images/content/disagree_button.png new file mode 100644 index 0000000000000000000000000000000000000000..9054b08fe263a27d634cbf335bf2a2b4bd7ae41f GIT binary patch literal 15683 zcmV-JJ-ot+P)pl07*naRCt{2y$66*)wMso+bOrsonB}INC%}Tiikl)#a>WjZ;2X- zEyj|l>3K#IpEc3s8Kcp}*fm(N0fG%dL5lRw^gg$pw#&Enxidr26!SF6`+wPv%sJwHc=?ffQtKQqsdak7Y023yZoU>^=J8Ivt zKfiQ#kas2L2+6;Y3(c+nz)Q0$t^ z2G09)*P6|7A^YF_-T1*%py!06{OV2JAHAVSXY~}V8cn=VjSqPE ziTJz)jLD$N%?6A+%&gAkb<;Cu;RC19KR!0_l@}wA{MJFzY{~5$yietO@04D0wdwfw z#ouF}yv)X(rih;~VFHxQcAa9d?pP#>8Cx}W{JBd$wgZ~D`7RiYV8&2ABvx|HMTjt2 z-Oog*>#VOO-+7r%91Y_XF{6kOwC@Re%o5q2Y>$(*gZ3G^b8D5v?bKucy?B|NGIr?B zu3jQ3q(X`nE^tV(1uvtwUP9GXdq39~QeR~A%gU;_Lf7x8XK;JMuv!B?5x_d??`N96 z=SnD`B(zg+#n>-Lj+%P;Hv`X8N_;MkDLjW?jhP0sC{b*dMiVKvPu7v|zjMH!9$$X^ zDIP;D)#R8T+?(0Zu<4Fl$=}h@vClr4fBcV!U2rk5@#;&y$%baS@!VGh!ijelB@nU) z7a3Lk$eSA{KlN;G-V+`37iJ%Lh%@P<$b|K8FP}2oKKaDqcYoJFYE!CQS50N`?jrYV zvvH7)KJx@CbF!vXQ1`%e9!OWmi#m6%iXkUoztfs9F|lx2+DfEcqmsAnxbCNecC7Z6 zmN9|dEgx0qvQVAjID;;_%>JK8xQl9X8739AWPq5Vdymn8wRNa=5?s)5gb9pBDU{u? z0u7%q;E9(IjQ@kzrD+-%8R5u9Q3a=Bxq*fSF-vk*8o0L)s*VZTOPIY_A-w&{(Ws{U zGoI`BW#MZJDM}9=ry1pzp|vx+<3qAHP3r0%)0{{x6>D2vXfQ<)Czohe554h#Sl4FLTH{N1W#BR8<>8{(8!sMhN2&mpo&see6Kd;z$cwlr3+URC%$kSV!kV{A(& zrYN~-TjlK84yhOCTv>DC0%zpF)WjnaOXpK$ueWK@qkqhP|Hu2UUDKE_OWxm9_p_@~ zm(DKx!To-%lxvB?VTPAWCtCg0B%lC6P8Fc%WjOQbFlZFoN$KlT?T+oQVQQlcsSSaWD3o-vy9nUE>OSih?WS-4 z%I#nJI)IY`&;aTY(3eUQRWe}U!}H#ThwR`H!;d$OlWx3~d5%^tVF|zY&($v&dQIS8 zOo(Trq!}O6bmcuAS#}B+C*F92Q@_m(0>QZg&H%|(pNmlFFH+|3_X-%3%s=sjIHIHe{QSBTPX>eg#ZXV7Z6ug-`dM(z z+)l(wwM~e{YmPpw^T}sob;1f1i~FK;&dWt(_6_`eaRwlbiqcTK}Rw^jzEW%z{3lMbqX{R5e)$MI6S zZj)g~BThDY{k7dczfD+NRE@X##qiP(s4!tFbVo5h?d|!Xr{H?Y&|!`dOx~8Ur5teN zkzM!xcY5jiI03-xJEfA5r5`Y$c8Rxlj+-Yxl>k^fN zKm4&tl0gF1Hg|3>9d=-D%g2>tCVzbY{L*oUw=CIKTr;3Ey(*rdgKI{u*;tpd#&kQY z#++C=vs}i9H@x^nRW~+5N_v3qSgb(NV%Htf;Wktx$oy9Z?{f%Wyu3(|1mY~hy<{K? zzK7z#Tet{NU||9RjaG_0zpHiv!K5M|17$wY>&n0Ub`fvqR{7R%e6Onr^foH*u7lV6 z9eVJNvo1(E1VC~bbfK<6D%VVt25Jm_#W3>1nN}*>hLdG*nT{rR z4oFQ~)?iQ`NIxO+Dc6cJ!IB<|i$H-$kRXjB5(kD9CYTtBf}92vD98`kFh2wGZyP?P zG0wMLf6;)*a{0e%1- zS!@7eFyqn+$Hz(=mw`S?V9HGzCy4U^n_5o|UcW`mm|zowFx-zJ`K$XVqwz~lj-Pm} zQQZ$Qpf&akW&}c6hE}-PF2$1A(S#{uO7KucKvx9D`*N}3=p*)fZC-%EF#z>Tsv z=norOJ6Ek>g-@oA9eJ?l#3-}ME5}WnhN8BP4us#8a`#ApexMNA+v)oB?Oq<*-@N^s z6@IDB?KZsLqE#&kb#n*`2$j<7=vcPsEu3{)D0!->t+k_*8HRDwpxRQQs=jj6=)(>} zV1T(sF;WKwKss;%&H&E+T)q9WhyTSkr`vb&NXMt7W-Vh@(m=YNM%eX8ejl~jv@057x~aB*Y;yM zQ`sDInzw!8L;$uEF#VvCOl`XT?g)zdHj_aB2vt%9b`@E+0s%A`3LF!a*Kw{H^j}j%v zKlaC0k2v8dbeJ($*@!Pkg9)BS?-hIHNuRC6BA89HFCtm=*|| za#_mptH1f$@h-Brm@s3WdYK5)8C5TVmcz1WaE)$W6 z1>LBt%S=|$k|Cqox3w==l%H`qi^pSI>$X4haCKwaK5tqc(c-H8%&rz}6eVU%2L`v+ ztT0yB6@f1{>p1tcbDEMV+MYK1CCf|vx?~b*L?_U({hbl#ytOYK0>S^(~X%^ zQWH*1opxkq>#89yJ96+e*fjl~pEocCBX`Raf>Oh?20TFd_N`@pz^MZQ+qrT%K#N$vRQ=###gwVQjE(D`#siY-tfH#wmLY?JxKFdi zm4l0!aKTzcf?#*!{vc@tMZS*xfOFBoB-7!-09Z^}vX6K$C#0>mmYjBG*R#)KVat7|c&9WZpPFkZ z74uQnH2mpP?UT=LdErIoa`uZ~b8wdyzLviJBJ12%PNH$}1S5LY8F$( zkG}r_J8Ng-T{;M)InvG`uQX?PpS;s>@ZnX2&nj_0K(D{0fA%cp;lG9mqk%g&#ZNtR*JtOR+%NncL8vsmNWMP@#mjqw ziWi@aE{2a(XVG9#7p=q`ppdWKZj^g{L1O$A{HI5%PdVdL!5&hLn$avxvV{EUj{e7<9>lRFi}k0#&|7!bUvwsU zcQH2AP@y5TC4EWYKjc$EFlARt@&vPeoAmdhc_wfVG71U#&&0vNW)#KqEl$ELHzT>g z1qq=RVW%Di6Kkkg-sXFJ<$v9r{sm4&=m8QFm-%^q?v)vJwW)+hg}}u2an52R15X$+ zFf*>zpbGvZ#U%C|} zQg{$L4~Q#Jz;q|hKbop~bNvR^$IF9is*f2xtf{UJ^U%O!y~LzKpBL|f_*!d{(4zcX zQ1OeZABAyw-1j=x5gAH)Y_yOH3=JX3Fj;BY+iN#Ax3(6D78-0qt~h8yX-lhL-*E7O zQ%i>pQH3Z`NInI2y{8@2qcFa}@%%fGulKFpsHv|A>4{uZs`*W|j}Rlprppk6PK(SZYA z@Voy{Fw^I%AL}(*Vt|Zc33^S4fP$ALzzwu8wJ4*Ag32QltZrT}jOLkf%9U5Yb?1K% zY;T`5ayTn#Zi!i}cE9oY_Kq-G#X(xN%O)j~1yR)sP-}xeUJRS=%@Nt_wkMXc+r!Uw z`#0;~Y;T@mVpbaDW6T~jY_v>ggkCik7@&>R9Ed^%D7EfWJ#;K6*MGOT)8BuKduF)r z7XGJRw#jiupL+6FW*i$xjXX%G)HGOWYkA|Bca?RfEi)wzj15Yt;aid6`;6M$(%9eQ z40tu^5e(v)?LvW2fe2JUIdHP}c``=c^i7W`CVF%Ov-Ad4PN9@_}5V20)uBQQLpUuUHtEF|2H+b3`YYGoQkfjf68wj5 zP;yl1)p)|J@~Y#1{EH=zKIkr8$`hqAY=NsQqJeE2rU;^r!we}SAnT2dG8RExC@zTb zxQ|nlCRZIaMIu))>TOuvxp7eitMn* z3Ot{QXPmn@lVu+*$FU+oEgX=bq27?lkc!z{+1T$`@p1WnW4RX)&avP)z?p^f?8?DI zPMr?E%lz-%TtqAr6k-@L1Lo$>%q=?MEVDA{bZ%aG+HAjU=pm2XL#`pq?z*#d{vwCb z07s0mLnA6rMV>pv4D%}g<)&lzTcT}a?w}jL=P=9EiJ~&@<8ANFt(ty}A*?XdOt0o^ zZ^*W%eeV6X*J ziLNX)0?>WnE#LA9c#t3}By*X{m9SUexixJtB(D_G~ntRhs%# z3olU6CbIE^w=2ht4mguCukD5u#McS81n^@vO-(yQ%RRV{2At@?0+wLSa~WzF(d>4s zJDV?x9eC$YH%>VvGii#c@>zmQb}X0J@L0vb%V%ZRw`JBYmYcWR$_fz( z(^C(Q*pcnBF{j-sNS2*7%kGz;i&nPg9b~xi73?h)<+2nE(i7k>JK_>TFXy<6pxX%RjXr4`bfvPb-*g3 zL{!Ka;?Bn_sS#ljcm}OXl+J1n0r1$^1mp{w=;;LE9_B)$OX1=JpTZ#f5{*Y`_1M`lsJJ{BXam`;e8ZKdGz%pf%#j1s$7~ZohW+m_^H{ zUvScr!;Xk<*%?F!PFnjWU%PV0{xgR)Y_2V`KECD(yS1LbmD+FpTp&&RpXMaLdb*^! zjraZxd4oqZy|w+yE4Dv7Z}Nw4ez^ab(x%2Xv*MsF3)Y-@X13Xy@c!cW$Br4)+1+WH zHs6DMK(Ro&l~X4JKVZ@;*-DgnTw-41(2C#AS^ju+)m=Z1pLwZo$1QI8HgeYPu;|v} z5Ios{29;z?a7!smqeEn)K>5DPFHH*x%q3obSS$8ZN&2iFdm+RwP?68X_OI{5Hm zr_RXTd{60|-w4Gv{q`Of%{RTewEwKbOB^T2rOR^Ln6_G^{>~q6yyLbJ-#9v%FF$DpB^Dx+9=yAG=B#oa_x9?)9s*~TU^+5S*4*&jSD$(2f|nKmyrGC$E{|z0(hHB45Ef_(s-!6pU>Y6% z-b1cSLtg z>s;^vqN_HQ8j;+$e!#gjCJu0v9Q5iUQ0S6jbNFcA<7Q1MNKC~`Zz@~{H* zyGTUQZpH_SC6K#gJ05XxpX_US+FKlL~+H147lY_e-`lWG?Q zd5RkZP>JFu*lq1r?4=zP@5O2np~1}_v&VZ<902p!fOV*fI4Cu8v|iB4?kIw~RlX3+ zi3(NZv^S4DsCeCCg9pRkd8={A1mZU02Y+7q;m48VUP{kfUUB9T-5A9lfm)X5#j3_~ALqh(>#w;}I!9gf;I3%ta`M{l+PWMG>thprLpI$FWAdG(G)D zqSr$Lfru41EjLbBe$UY%u#e3TKimJvGZ^X#J4u5&h~&)ZPO~fegR90}^Hb?Zs9wbI z1lZ};)`W6EITEB7fIU!a1cC0;u>?zrzSKRe3{HLU0kv@i90(i$@Q2C>hxq~Ru0Ssu zH~@Zt?@$qhh?-^@w)!Qb%G-F%nFFNJ7N%4K&ue_tZ-U#nq|P+TR;)`7A6$OUH0RbQ z`-?8*G-k$JY5ZXx)TOg*F)qn%Tz||r$_^V|e9IL(k2o^9Y)J`;v|cfuCl+u;Y?z%OATx^}EL!N{X$vB39HE zT|wJ!{7LMN@7K(EFnw7(u!o_JrOE(k#dOB!o+T<+ImF=$; z)wO#2hTd_Y6SQm~l?PzW?Yisk3Qq%Aq+<#&e}ZZHDgop(knB@zhP2a6y0cF9N2QD> zU(H#T`NJ#oe|kB#VS9w@T+@!Y<(+medw(7UlLKzayt&D2uIsY%;;+1(GN~C95#{D@ zydeAh3(x?*@ruMRo~eK2jhZDNnj;4{oPAQ&)@|LYDZ6kL8k?$|G`w-daV4AQM{279 z!T5)cwgPi-uBo6IbeLn+JLbOAvbSH-{LG6)rtt-5smGrRS6yZ08e$oE*Q!C~yhT;17oc^1 zQ~&bFxC5I?S^5=pkmec*qtqpgqiitkAqL zfE(fZoYsP1^LH~dA)lkDSGXfo%aGr5jlvBP2qi^`8-QkOw)*e9Y0sL)^x_z*NBst? z&8c>tH^9Bu*?p#dJwyXK`UBD)=883T^33Dde+0ua?&N(p?F12 z5!*I&_~^AOip`j*e5?~?NSIguML6Z3Yk0tj22S!UJ^5GxZnxI4$Tn<05=bZNv5>94 zVDi!4@Fi)F(3FE6i;V|=bz`dwBI{#;ivVww`YIcdF(G}^D2N~!+6+N$%G)tuF#F+$ zA5~SC8M$17M42IwFfF;ak^1MB2-*{4wCeSk(G}GXv^;|z!YEA(P60|Y z&2bLX$^y&;Cp5!ECI~*N9Qd0>i!`+OsiZfQ7x#BrvGSP#5ZFNfiU`ubqPap8W>)}v zuUn;cTz*WN<9vne;+N8dkp{e1&LyR90k-Tpof*PP547C?EcNN*Yj}G;XZdXTSVTmbBivNo>{HOf;6xBPMdr56r zZ~@7DmUQ(LaI9Rw&3df^WK}Pe0TA;^gq4=OyKVc1me#!E^u64}e5PrB{rIEk_N}oT zIK@I_eHJMhbNIpICLb6tF4k^Jh-G@#&3j94fBSxa3YsE?KI7qk%KxRZn=kUKvHNADUNz5mr(7(nVHSRYry~rxcN=~ecj*|Cr=!fB2O7C0YcmMCJqA$BSwA+k?6AW`3_#)RWWyO9IrXGKy zT~nRcnN?H~gE34@9g$FdgAZ9m{{VahtsEdOxrv2d9sy-< za_TZOAS4p#0IrW+OL|1_)V58mm#)A+_o!ZY1+{JnK%U^eV_#8fMMX*6ak^Eet3*YKGfL-P?HKpdZkc>tBD{r&BH%R#*2TlCO zC88(Di)7HTZr)tf8I+XOl#Lr_R#gM)$hS5&E?v>^${VP@E<$Das;SQ!R3yvnF;~t` zO`9zA5l4RZlaH6)eiz9FTEdXp6#@8^?(KPq|LUjzz*qh^pZ+VFqXOGXwMF%*Lb8h+ zqsGj-`P;Ir+659T6fR#PPN@oz3Zycv3?(peNlx`{$etnUz6Hi>Ck3nG;M;28k5|&OC@S9hys7As_ zB)QJ9)wY)i8LB-=g+5!zP|`xuZ*Fay|3S%!f#K05fEwJ~g7?e~J1l0B&@zXoQijS8 zl(lxebyu}F-D#79L*cPC1t`lc0B!}RaKp=t;#iS+xW~gR#TI$EKkQG^)3KaFzAKw zFjh$J=7xvXT=z={votE!oeixu?lIw^%oMOguO07Mv?dsoOc+(=z$GW7j= z07BR(+Ep{|gaNgGU=lP(K!7eZyK_;LC5FIM>R9eyn$m=331c~aMO7XIUi%oLp}>4 zKs9E7&9H^uN1XxLW~91{bAnx_bBRKp=Yc8n76`4f9MBDjabjDRtB(Yd26%H+uXCH8 zp73r1C4C-Mf=6&*=)^0D_1OVJ^CSWV5e@Vfxh{$+=N<^5#!|Lqm}+Mb1O6s@)m{sd z>f^+}P+(Slf?Ih8*VZ0*#ka6sqUS6;a*Fhv{1axzAR1F9$@u(<7dQOw<@Sh5O_(tB zt7jA+JHsui0xe|gECFQWf`x-#000Z%H%mnKzxJ~CuD-!+YgW-1r!toAYF@t{rPC=4 zbl_Zpne{WsW=tr92up7lB$k##6+?ll+tgXVg~b!GWQl1;LCO>fq>zO;3yhcN3Ao}q zR{`{bmKDVuKa$OuMi6pbpSbOvPr`VB-7Mt=c_m?Hg3r8+mCq2UlENcw@J!2dkEM@2 zBsr{>>D)rQbjES%PnLQQJWne7siRLFaM{^|b>fK$^56X0OASBzVQB#Jl+0A@*Z)UX zmrmO^zjH&^#~X^r><8$P#AWd6>kYU6GDUITKo0b{q^9)3i^^vlGI0IQd^b-XG|{Xo zi+btohSi(z`Bi+;Cq|Tk^OV#_iOBl?mC4I49(KgBsyNB(wzYlyN#*3pthikI!0#9Md6PlET?4!!QC(wT>Namzrt#$Vsd@Be*- zn7}{HcC7r|Q~I5AvSG4@#UI8;4@(W&r*qMJJAeN3__oc^zH4$zc$UL<7FXC;Ts-9T znbb))Em;fT#HuK5ocGGM`|eBBx5t$}jlrcFl2O^C)&C;rp%4NI-{i92$;TU|)qwdi z5om9Ns!tJ!_{8v4l>7a2Xk;gM+UDmLyw#`-UNxZPw3DkZIF%0{DR6>l%@yvs(kH@# zqQbO)&49zdlK#zuj@{uk=e@j_00b{ZAv(xIO9&QKr&Sq}8c2Y>kAw;HGPDgKg`}es zcXSq%o+LddxrT8^>E?;iq0a%zLOdDo{CQOcv-#0W3zI?;TzKTMVmHwOSiWS9bNv_BXGE4 z?sJl^+ZOT6{62?{fAU_hJQ}obTmAJ*N*657nRZLbxbZLFCvB9guQNe?cxK6uZtQpC z<>1W)OWxXi^|{Hdt?g>yz(*ctryXjIoHF{i5%I#_riSp zhWmfka^p=g+xS2Z8S%nXu|vjMGL~7jVd)uXme%j^6U=V3yH<52rc8{C9d3A*!d}J2 z=NC_3xALKTCzhp(r%%Gjj(DPT=hil5C(gd=!2kYL!=kXn1yD%>0wxIvO-#t=%+N>!vr?3A0 zPwVb|v|1`=Ae~#R5%ym@f(+{e^`!DW&}+#N!=ye|SZ7G;W6pLDG6P|$YbiJ7`b9#3 zX14@l9*G5_90hd8#>~BUuldcrElE3e(Bxs~Uzj@NP@X8GN~>!MF9Jp!IG|-iy-Ee8 zE(M{`Y^5;*wPYA7(Wixg+0|e!FqMet)H9;bV$)e_yMiTpsRV@q9<qo%s;vg7 zh(ZL2xZup#f~dcQf(xuD0Oe{%{~Sml8Hb9u7!rR1ySG;J3^mm0|p9Iv-UMW z^~QW(+iT(#d4*F$z%(jTrOOpKWdqeo8Jw({eM5HctGRj0 zsxH1}@Qqgn2uv|;UGPTQ1KJhkrSEQD8AsDOE*&-d%FVCLA#3X*DoL>87~JIME=EJ2 z!hHHz2GDe7Hw7@|hNrM9kNK9~Ikk)Vh)%AvxU#aQne=?H=f0i?Wk?3~cns$)a2S1T z6`7U&%+d;M#PkU+La*oo=bB6i<$xxM2800de9>bEvG4&_D0r@-#AQTjegTm&z_XFE zXVZNTZ2n7;BprfLJwU!^OcWd!05@U$?K6Wec%q|y>npS!5{86oY6 z&{U$b3clgn8ks7TYo7O#%KAlMjHIV`B!SxF+Vu~m-K`Kjzx?e_jyiStZ-3$LHx{5; zBtB%H@BR$k{9kgzvQ4wEF!I<$B{Ak1S6|V3#Wy=(u%*>SUq36gxxRF3Mx^xaNh6#m zU}JFWW0+?f>pFhESx;v+w85M#`^%5yI4 zf7%(mxKh6twqoh9NB}lH0ATGoU9T;)M9z>#G_W)l4XYI4!iUZir&{mm#CoQrPG}bJ zeH5u5ZG1mt1dUc z^>tpH3PfNG9zXKU1x**6Yd!WHHmvQ*nzGyfYwUSv!TX(WzqsY#k+D{idh{7{!dM^c zBY$B0DbI{5lP5YO_uRDVkvn21U0C|<%LiP4hkxem?v-07Oq~EYHT&9gEqC2*zV+S! zL<5rGn52RX2~vf*;28Kp8o6AIbLNMs=mh9YjDUgf(VlM@7`j@NA-kVpKlfZoT{_Sj zQwK6RAzTENO17HBXlm1hqTn6jXd;z+{DopSCy0pqBu`Di;vfk&FsJ5tkP*fuGe0A5 zd#{;PF6A|fw*BVW{IRFofZMe3`V{kF%mF2v)U(Ns2vVhqen*_T&pGE+jUB6uBAs-M z1F8b;apIn3VGt1-0059(AFpzrex=k90aljKl2~9Uq2Ja)JBGE0f~Ew&8pi-PhwBAQ zD)K4z0iG24ZB8|!EbPYTE^>V>;vjG`pb^4cTR}@f$mH7HBVo#U??G5HtXAn7e4+R2 zNaR7!z?Sym!Wa2d&l>c=FTkdDn=0AW)c&n&#beK>0Hn(xZ+9DR8OJ;a zb^&XW!RP~vQ;Xhqm8xMf}h6B0v9e=TCmvHR0I7&pwO>CnU-kCU_*+t=@0s z8&4U-1_Fi%x_7kSaO2R%Y@6BNjK^FcjbMzq*t5VQfeDMcU_9*dlfG8|%bPpzzO`x8 ze}S>K^m)1ZSZ+dS6~?ZKX@K-L9i{h|m^u#}J|Eyq(;yv2f`KtMb>=z=|E2}dMA*uz z``x4N$w#vTCIgnS2OU(o=!NFfFHU~6hJ#9zM(5bEHTT?-yY{-umFpXRb9d~d>2&BI zDLFal@X9xy+G%hzrXz_LWcpCiZq6_GT*0BzDg9 zOSi6HamgiPpL&E=42g#8sK6sSz@|^=0@ia{EY2p5JmB2(2ON1Kjh8D`I2k%D8j4v; zFMJ4{2T%191OviY%vrng^>2WM~j5U{zE#=)KpvSALRzb{<~4xaNc@pmH#` zO1^$ZX8ge5@*kqr>(rG$skrxg;^m62x!#z03ibmUo;n(;dg`uQ@4dUY>$R7nb=wVw zGcu8xIH`D0EeZ{a0hVCw_?G1BH8}?~gBd1@=*@4Y)30r7e#%L?TW%b9%T?fo*tL_( zKU__88ZuPfLGQlTJ^GX=meN2&Hm>O&b1=XAwu*0@7gbD5m@#b4wt~foqICKv-_M+Q zc8!u8U==pf28waeMt|eXV~J&yb5O}y3&oZ!UpM*a;eWh`PoJ4r8Z*VDc3rd*K<0(_ z{1z&66wV@P%dCbJ$lgq+?qaUZ8dL=16ne>|ZbS1SlMLwt&vOO(pM&;F zQVR?P7)Gf#l7Mkgq2>dBimEWj29#`7-VYwSzwWp90g|vp_{SQUqmPmxdQ+4~cz2-3 zM<|XMIO*$PEzlB_qwb&nt1AI&UcKYM{b(2y)hl^KlxolbR}FH;q3vA_fFAAQanP%5$|Q^`+JH;~$9u)iC{{u{oN~ zVv23d>RfX7Ee zLWwfqHj4CKU%l!B)I+wwzRnhJ>Hp&!x_@+iXCfhMssO&n9n{@W$NY?8gQ#izEv4<~ zz3UChD=uuk{8FB@BNh70c|cX7ZhO)bgD?X{l{Zzymk}9Q3M!U;p}N0RxSlN*)qjgb zxWun*YuIws>HP4S<8HkfPaLOJkiab5!6cLq{9*@S3W&!{oh7wrRr(Y}fs^3enk2pP z-Ho%as#&~xKMd#@?3M}22RHV*pYN`>FW=DX@6x=_d571{ZpXntCXix0b zitKZi46&DBfg$$7MKrj~@8PejzF*wnyl8JC1iLEeyXvc3=%E)bS@?H#^aegqmOkGj zmebR2@7LQK!sGu0>S#jxXi(Ji$3lFlSM+Xf5Gd22fBK5g4FsA1p>^Foqt%<u%KaG{kLcP%U2mX-b9HG2msBAF@0v)cW>e&`x*WB0rj79!N0>6@9m%J-7HmT zG%VpU@j7T+Xa{Igl<7q4I}7(cBy#$w;F>{!PO<{IC=EYddyr6X9WK-<3Q?~@wR=-~ z&%%0l725#`^%yT9sOzmcTMwR-9z3}A(#uOvIbBp$6I=huByb0bUNPbzOdv5FXv0HO zt5-8fsQ-Fn(?KbdV2+r~7Hx2MiqsbYraK!2P?I zeq5!GAr&AD;PKJW3)r*6`e4()KBV6FzLe6tHumMz{;5KwcEU1dsWpBwuPl!E!k&I$ zuA?>B)KKPvlh1Qe8XG?vPe0V^Z*<-JeRT8fMI3yLxQ=G&U2|U&8NR5Z_L^U?b;TYl z0?oW>Xa0tpHh=e~j)-ZD7@ORGV$lKn#zv21C1r|5^uk`?_~{O3bnXAsNkCd@QztUlY*!bE|baBdY+df>EoOgkHv52#Tz$yKw z4Xunm22y+(d&R!+vWIx+6eY$i!I=~W7S#yXuY$`)j899qj>`(RmipJ5kO zRS)cL%v+8V#1ayC+z&(E`Vevw#uXLXGM-hP>+#a{yNh5M+!&Ul zx95Wq6^{Na{G%lF-(OJiXX<8;8E&}5TUaHCNvzKwK)q(KSDk5K(|gK5GzgD=GxQJY zO}6luUT9FgX&2hJ5Jt{Gc?u9xc4JFuZYc^!%Ynb}=`$z(Q~pyF-pwZVhO!HpHj?xtA9H;)8Bro9gx8E%ea*np!n+cb{s{?bUwDQ8X|9lj z@`a>E3!T!0_AKGfi!dP#n2&?=Aw{0^Y(-U5s(4;Q)3ViT`d-~*-)=tdytZ!RYrt@Z z5jBm2#*Pls>GtLpq+@IZ?=hCoEWPP0Um*Sdfv^0hxxezT@A*Gc@TY6uiyQj*5;%`p pX8*y1wsmxDYi-MXvi{T6{eQ<+SaZHo=zstK002ovPDHLkV1h1w-}C?g literal 0 HcmV?d00001 diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js index 8c5ccbabe..3d52d2139 100644 --- a/web/app/assets/javascripts/accounts.js +++ b/web/app/assets/javascripts/accounts.js @@ -8,10 +8,10 @@ var rest = context.JK.Rest(); var userId; var user = {}; + var screen = null; var gearUtils = context.JK.GearUtilsInstance; function beforeShow(data) { - console.log("beforeShow", data) userId = data.id; } @@ -71,8 +71,11 @@ invalidProfiles : invalidProfiles, isNativeClient: gon.isNativeClient, musician: context.JK.currentUserMusician, - webcamName: webcamName, - sales_count: userDetail.sales_count + sales_count: userDetail.sales_count, + is_affiliate_partner: userDetail.is_affiliate_partner, + affiliate_earnings: (userDetail.affiliate_earnings / 100).toFixed(2), + affiliate_referral_count: userDetail.affiliate_referral_count, + webcamName: webcamName } , { variable: 'data' })); $('#account-content-scroller').html($template); @@ -135,6 +138,7 @@ $('#account-content-scroller').on('avatar_changed', '#profile-avatar', function(evt, newAvatarUrl) { evt.stopPropagation(); updateAvatar(newAvatarUrl); return false; }) $("#account-content-scroller").on('click', '#account-payment-history-link', function(evt) {evt.stopPropagation(); navToPaymentHistory(); return false; } ); + $("#account-content-scroller").on('click', '#account-affiliate-partner-link', function(evt) {evt.stopPropagation(); navToAffiliates(); return false; } ); } function renderAccount() { @@ -187,6 +191,11 @@ window.location = '/client#/account/paymentHistory' } + function navToAffiliates() { + resetForm() + window.location = '/client#/account/affiliatePartner' + } + // handle update avatar event function updateAvatar(avatar_url) { var photoUrl = context.JK.resolveAvatarUrl(avatar_url); @@ -203,7 +212,7 @@ } function initialize() { - + screen = $('#account-content-scroller'); var screenBindings = { 'beforeShow': beforeShow, 'afterShow': afterShow @@ -218,4 +227,4 @@ return this; }; -})(window,jQuery); \ No newline at end of file +})(window,jQuery); diff --git a/web/app/assets/javascripts/accounts_affiliate.js b/web/app/assets/javascripts/accounts_affiliate.js new file mode 100644 index 000000000..21d0b8b1c --- /dev/null +++ b/web/app/assets/javascripts/accounts_affiliate.js @@ -0,0 +1,324 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.AccountAffiliateScreen = function (app) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var userId; + var user = {}; + var affiliatePartnerTabs = ['account', 'agreement', 'signups', 'earnings']; + var affiliatePartnerData = null; + var $screen = null; + + function beforeShow(data) { + userId = data.id; + affiliatePartnerData = null; + } + + function afterShow(data) { + + rest.getAffiliatePartnerData(userId) + .done(function (response) { + affiliatePartnerData = response; + renderAffiliateTab('account') + }) + .fail(app.ajaxError) + + } + + function events() { + + // Affiliate Partner + $("#account-affiliate-partner-content-scroller").on('click', '#affiliate-partner-account-link', function (evt) { + evt.stopPropagation(); + renderAffiliateTab('account'); + return false; + }); + $("#account-affiliate-partner-content-scroller").on('click', '#affiliate-partner-links-link', function (evt) { + evt.stopPropagation(); + renderAffiliateTab('links'); + return false; + }); + $("#account-affiliate-partner-content-scroller").on('click', '#affiliate-partner-agreement-link', function (evt) { + evt.stopPropagation(); + renderAffiliateTab('agreement'); + return false; + }); + $("#account-affiliate-partner-content-scroller").on('click', '#affiliate-partner-signups-link', function (evt) { + evt.stopPropagation(); + renderAffiliateTab('signups'); + return false; + }); + $("#account-affiliate-partner-content-scroller").on('click', '#affiliate-partner-earnings-link', function (evt) { + evt.stopPropagation(); + renderAffiliateTab('earnings'); + return false; + }); + $("#account-affiliate-partner-content-scroller").on('click', '#affiliate-profile-account-submit', function (evt) { + evt.stopPropagation(); + handleUpdateAffiliateAccount(); + return false; + }); + } + + function _renderAffiliateTableSignups(rows) { + rest.getAffiliateSignups() + .done(onAffiliateSignups) + .fail(app.ajaxError) + } + + function _renderAffiliateTableEarnings(rows) { + rest.getAffiliatePayments() + .done(onAffiliatePayments) + .fail(app.ajaxError) + } + + function _renderAffiliateTableLinks(rows) { + $screen.find('.affiliate-agreement').on('click', function () { + renderAffiliateTab('agreement'); + return false; + }) + + $screen.find('.affiliate-link-page').attr('href', '/affiliate/links/' + affiliatePartnerData.account.id) + + $screen.find('select.link_type').easyDropDown(); + + var $linkType = $screen.find('.link_type') + + $linkType.on('change', function() { + logger.debug("link type changed") + updateLinks(); + }) + + updateLinks(); + } + + function onAffiliateSignups(signups) { + + var $table = $screen.find('table.traffic-table tbody') + $table.empty(); + + var template = $('#template-affiliate-partner-signups-row').html(); + context._.each(signups.traffics, function(item) { + var $link = $(context._.template(template, item, {variable: 'data'})); + + var $day = $link.find('td.day') + + var day = $day.text(); + var bits = day.split('-') + if(bits.length == 3) { + $day.text(context.JK.getMonth(new Number(bits[1]) - 1) + ' ' + new Number(bits[2])) + } + + $table.append($link) + }) + } + + function onAffiliatePayments(payments) { + var $table = $screen.find('table.payment-table tbody') + $table.empty(); + + var template = $('#template-affiliate-partner-earnings-row').html(); + context._.each(payments.payments, function(item) { + + var data = {} + if(item.payment_type == 'quarterly') { + + if(item.quarter == 0) { + data.time = '1st Quarter ' + item.year + } + else if(item.quarter == 1) { + data.time = '2nd Quarter ' + item.year + } + else if(item.quarter == 2) { + data.time = '3rd Quarter ' + item.year + } + else if(item.quarter == 3) { + data.time = '4th Quarter ' + item.year + } + + data.sold = '' + + if(item.paid) { + data.earnings = 'PAID $' + (item.due_amount_in_cents / 100).toFixed(2); + } + else { + data.earnings = 'No earning were paid, as the $10 minimum threshold was not reached.' + } + } + else { + data.time = context.JK.getMonth(item.month - 1) + ' ' + item.year; + if(item.jamtracks_sold == 1) { + data.sold = 'JamTracks: ' + item.jamtracks_sold + ' unit sold'; + } + else { + data.sold = 'JamTracks: ' + item.jamtracks_sold + ' units sold'; + } + data.earnings = '$' + (item.due_amount_in_cents / 100).toFixed(2); + } + + + var $earning = $(context._.template(template, data, {variable: 'data'})); + + $table.append($earning) + }) + } + + + function updateLinks() { + var $select = $screen.find('select.link_type') + var value = $select.val() + + logger.debug("value: " + value) + + var type = 'jamtrack_songs'; + if(value == 'JamTrack Song') { + type = 'jamtrack_songs' + } + else if(value == 'JamTrack Band') { + type = 'jamtrack_bands' + } + else if(value == 'JamTrack General') { + type = 'jamtrack_general' + } + else if(value == 'JamKazam General') { + type = 'jamkazam' + } + else if(value == 'JamKazam Session') { + type = 'sessions' + } + else if(value == 'JamKazam Recording') { + type = 'recordings' + } + else if(value == 'Custom Link') { + type = 'custom_links' + } + + $screen.find('.link-type-prompt').hide(); + $screen.find('.link-type-prompt[data-type="' + type + '"]').show(); + + if(type == 'custom_links') { + $screen.find('table.links-table').hide(); + $screen.find('.link-type-prompt[data-type="custom_links"] span.affiliate_id').text(affiliatePartnerData.account.id) + } + else { + rest.getLinks(type) + .done(populateLinkTable) + .fail(function() { + app.notify({message: 'Unable to fetch links. Please try again later.' }) + }) + } + } + + function _renderAffiliateTab(theTab) { + affiliateTabRefresh(theTab); + var template = $('#template-affiliate-partner-' + theTab).html(); + var tabHtml = context._.template(template, affiliatePartnerData[theTab], {variable: 'data'}); + $('#affiliate-partner-tab-content').html(tabHtml); + + if (theTab == 'signups') { + _renderAffiliateTableSignups(affiliatePartnerData[theTab]); + } else if (theTab == 'earnings') { + _renderAffiliateTableEarnings(affiliatePartnerData[theTab]); + } else if (theTab == 'links') { + _renderAffiliateTableLinks(affiliatePartnerData[theTab]); + } + } + + function renderAffiliateTab(theTab) { + if (affiliatePartnerData) { + return _renderAffiliateTab(theTab); + } + rest.getAffiliatePartnerData(userId) + .done(function (response) { + affiliatePartnerData = response; + _renderAffiliateTab(theTab); + }) + .fail(app.ajaxError) + } + + function affiliateTabRefresh(selectedTab) { + var container = $('#account-affiliate-partner-content-scroller'); + container.find('.affiliate-partner-nav a.active').removeClass('active'); + if (selectedTab) { + container.find('.affiliate-partner-' + selectedTab).show(); + $.each(affiliatePartnerTabs, function (index, val) { + if (val != selectedTab) { + container.find('.affiliate-partner-' + val).hide(); + } + }); + container.find('.affiliate-partner-nav a#affiliate-partner-' + selectedTab + '-link').addClass('active'); + } else { + $.each(affiliatePartnerTabs, function (index, val) { + container.find('.affiliate-partner-' + val).hide(); + }); + container.find('.affiliate-partner-nav a#affiliate-partner-' + affiliatePartnerTabs[0] + '-link').addClass('active'); + } + } + + function handleUpdateAffiliateAccount() { + var tab_content = $('#affiliate-partner-tab-content'); + var affiliate_partner_data = { + 'address': { + 'address1': tab_content.find('#affiliate_partner_address1').val(), + 'address2': tab_content.find('#affiliate_partner_address2').val(), + 'city': tab_content.find('#affiliate_partner_city').val(), + 'state': tab_content.find('#affiliate_partner_state').val(), + 'postal_code': tab_content.find('#affiliate_partner_postal_code').val(), + 'country': tab_content.find('#affiliate_partner_country').val() + }, + 'tax_identifier': tab_content.find('#affiliate_partner_tax_identifier').val() + } + rest.postAffiliatePartnerData(userId, affiliate_partner_data) + .done(postUpdateAffiliateAccountSuccess); + } + + function postUpdateAffiliateAccountSuccess(response) { + app.notify( + { + title: "Affiliate Account", + text: "You have updated your affiliate partner data successfully." + }, + null, true); + } + + function populateLinkTable(response) { + $screen.find('table.links-table').show(); + var $linkTable = $screen.find('.links-table tbody') + + $linkTable.empty(); + var template = $('#template-affiliate-link-entry').html(); + context._.each(response, function(item) { + var $link = $(context._.template(template, item, {variable: 'data'})); + $link.find('td.copy-link a').click(copyLink) + $linkTable.append($link) + }) + } + + function copyLink() { + var element = $(this); + var $url = element.closest('tr').find('td.url input') + $url.select() + + return false; + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + app.bindScreen('account/affiliatePartner', screenBindings); + $screen = $('#account-affiliate-partner') + events(); + } + + this.initialize = initialize; + this.beforeShow = beforeShow; + this.afterShow = afterShow; + return this; + }; + +})(window, jQuery); diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index 474503da2..d9ca26642 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -10,6 +10,7 @@ // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD // GO AFTER THE REQUIRES BELOW. // +//= require bugsnag //= require bind-polyfill //= require jquery //= require jquery.monkeypatch @@ -36,6 +37,7 @@ //= require jquery.custom-protocol //= require jquery.exists //= require jquery.payment +//= require jquery.visible //= require howler.core.js //= require jstz //= require class diff --git a/web/app/assets/javascripts/checkout_order.js b/web/app/assets/javascripts/checkout_order.js index 70f5171a0..168b965d6 100644 --- a/web/app/assets/javascripts/checkout_order.js +++ b/web/app/assets/javascripts/checkout_order.js @@ -185,7 +185,7 @@ displayTax(effectiveQuantity, unitTax, 1.99 + unitTax) } else { - + checkoutUtils.configureRecurly() var pricing = context.recurly.Pricing(); pricing.plan_code = gon.recurly_tax_estimate_jam_track_plan; pricing.resolved = false; diff --git a/web/app/assets/javascripts/checkout_utils.js.coffee b/web/app/assets/javascripts/checkout_utils.js.coffee index 7f61f0fb8..4570bd052 100644 --- a/web/app/assets/javascripts/checkout_utils.js.coffee +++ b/web/app/assets/javascripts/checkout_utils.js.coffee @@ -8,6 +8,7 @@ class CheckoutUtils @rest = new context.JK.Rest(); @cookie_name = "preserve_billing" @lastPurchaseResponse = null + @configuredRecurly = false init: () => @@ -54,5 +55,10 @@ class CheckoutUtils return carts[0].product_info.free + configureRecurly: () => + unless @configuredRecurly + context.recurly.configure(gon.global.recurly_public_api_key) + @configuredRecurly = true + # global instance context.JK.CheckoutUtilsInstance = new CheckoutUtils() \ No newline at end of file diff --git a/web/app/assets/javascripts/corp/corporate.js b/web/app/assets/javascripts/corp/corporate.js index 989e7bb85..5d6e33bfa 100644 --- a/web/app/assets/javascripts/corp/corporate.js +++ b/web/app/assets/javascripts/corp/corporate.js @@ -1,3 +1,4 @@ +//= require bugsnag //= require jquery //= require jquery.queryparams //= require AAA_Log diff --git a/web/app/assets/javascripts/dialog/banner.js b/web/app/assets/javascripts/dialog/banner.js index 7e0fc0f12..2c777c0f0 100644 --- a/web/app/assets/javascripts/dialog/banner.js +++ b/web/app/assets/javascripts/dialog/banner.js @@ -133,20 +133,25 @@ if(options.buttons) { context._.each(options.buttons, function(button, i) { if(!button.name) throw "button.name must be specified"; - if(!button.click) throw "button.click must be specified"; + if(!button.click && !button.href) throw "button.click or button.href must be specified"; var buttonStyle = button.buttonStyle; if(!buttonStyle) { buttonStyle = options.buttons.length == i + 1 ? 'button-orange' : 'button-grey'; } - var $btn = $('' + button.name + ''); - $btn.click(function() { - button.click(); - hide(); - return false; - }); + + if(button.href) { + $btn.attr('href', button.href) + } + else { + $btn.click(function() { + button.click(); + hide(); + return false; + }); + } $buttons.append($btn); }); } diff --git a/web/app/assets/javascripts/everywhere/everywhere.js b/web/app/assets/javascripts/everywhere/everywhere.js index 1b27ba754..43a844a32 100644 --- a/web/app/assets/javascripts/everywhere/everywhere.js +++ b/web/app/assets/javascripts/everywhere/everywhere.js @@ -20,6 +20,11 @@ var stun = null; var rest = context.JK.Rest(); + if(gon.global.web_performance_timing_enabled) { + $(window).on('load', sendTimingResults) + $(window).on('pagehide', setNavigationStart) + } + $(document).on('JAMKAZAM_CONSTRUCTED', function(e, data) { @@ -245,4 +250,63 @@ } } + function setNavigationStart() { + if(!window.performance || !window.performance.timing) { + try { + window.sessionStorage.setItem('navigationStart', Date.now()) + } + catch(e) { + logger.debug("unable to accesss sessionStorage") + } + } + + } + + + // http://githubengineering.com/browser-monitoring-for-github-com/ + function sendTimingResults() { + + setTimeout(function() { + var timing; + var hasTimingApi; + + if(window.performance && window.performance.timing) { + timing = window.performance.timing + hasTimingApi = true; + } + else { + timing = {} + hasTimingApi = false; + } + + // Merge in simulated cross-browser load event + timing['crossBrowserLoadEvent'] = Date.now() + + var chromeFirstPaintTime; + if(window.chrome && window.chrome.loadTimes) { + var loadTimes = window.chrome.loadTimes() + if(loadTimes) { + chromeFirstPaintTime = true; + } + } + + // firstPaintTime is in seconds; convert to milliseconds to match performance.timing. + timing['chromeFirstPaintTime'] = Math.round(chromeFirstPaintTime * 1000); + + // Merge in simulated navigation start, if no navigation timing + if (!hasTimingApi) { + try { + var navStart = window.sessionStorage.getItem('navigationStart') + if(navStart) { + timing['navigationStart'] = parseInt(navStart, 10) + } + } + catch(e) { } + } + + context.JK.GA.trackTiming(timing); + }, 0) + + } + })(window, jQuery); diff --git a/web/app/assets/javascripts/ga.js b/web/app/assets/javascripts/ga.js index 8e51a081e..b26cf777a 100644 --- a/web/app/assets/javascripts/ga.js +++ b/web/app/assets/javascripts/ga.js @@ -284,6 +284,37 @@ logger.debug("Tracked Jam Track Play") } + function trackTiming(timing) { + + if(!timing) {return} + + try { + var computed = { + dns: timing.domainLookupEnd - timing.domainLookupStart, + connect: timing.connectEnd - timing.connectStart, + ttfb: timing.responseStart - timing.connectEnd, + basePage: timing.responseEnd - timing.responseStart, + frontEnd: timing.loadEventStart - timing.responseEnd, + domContentLoadedEvent: timing.domContentLoadedEventEnd - timing.domContentLoadedEventStart, + windowLoadEvent: timing.loadEventEnd - timing.loadEventStart, + domInteractive: timing.domInteractive - timing.domLoading, + domComplete: timing.domComplete - timing.domLoading, + domCompleteToOnload: timing.loadEventStart - timing.domComplete + }; + + logger.debug("page load time: " + computed.frontEnd) + context._.each(computed, function (value, key) { + if (value > 0 && value < 60000) { + context.ga("send", "timing", "NavigationTiming", key, value, null, {'page' : '/' + window.location.pathname}); + } + }) + //context.stats.write('web.timing.navigation', computed) + } + catch(e) { + logger.error("loading times failed in ga.js", e) + } + } + // if you want to pass in no title, either omit it from the arg list when u invoke virtualPageView, or pass in undefined, NOT null function virtualPageView(page, title) { @@ -445,6 +476,7 @@ GA.trackBand = trackBand; GA.trackJKSocial = trackJKSocial; GA.virtualPageView = virtualPageView; + GA.trackTiming = trackTiming; context.JK.GA = GA; diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index 175a96fd6..85ebcc250 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -52,7 +52,8 @@ JAMTRACK_DOWNLOADER_STATE_CHANGED: 'jamtrack_downloader_state', METRONOME_PLAYBACK_MODE_SELECTED: 'metronome_playback_mode_selected', CHECKOUT_SIGNED_IN: 'checkout_signed_in', - CHECKOUT_SKIP_SIGN_IN: 'checkout_skip_sign_in' + CHECKOUT_SKIP_SIGN_IN: 'checkout_skip_sign_in', + PREVIEW_PLAYED: 'preview_played' }; context.JK.PLAYBACK_MONITOR_MODE = { diff --git a/web/app/assets/javascripts/helpBubbleHelper.js b/web/app/assets/javascripts/helpBubbleHelper.js index eb68a4a6c..c8ccdc4cf 100644 --- a/web/app/assets/javascripts/helpBubbleHelper.js +++ b/web/app/assets/javascripts/helpBubbleHelper.js @@ -62,6 +62,24 @@ } } + helpBubble.rotateJamTrackLandingBubbles = function($preview, $video, $ctaButton, $alternativeCta) { + $(window).on('load', function() { + setTimeout(function() { + helpBubble.jamtrackLandingPreview($preview, $preview.offsetParent()) + + setTimeout(function() { + helpBubble.jamtrackLandingVideo($video, $video.offsetParent()) + + setTimeout(function() { + helpBubble.jamtrackLandingCta($ctaButton, $alternativeCta) + }, 11000); // 5 seconds on top of 6 second show time of bubbles + }, 11000); // 5 seconds on top of 6 second show time of bubbles + }, 1500) + + + }) + } + helpBubble.clearJamTrackGuide = clearJamTrackGuideTimeout; helpBubble.jamtrackGuideTile = function ($element, $offsetParent) { @@ -82,4 +100,33 @@ }) } + helpBubble.jamtrackLandingPreview = function($element, $offsetParent) { + context.JK.prodBubble($element, 'jamtrack-landing-preview', {}, bigHelpOptions({positions:['right'], offsetParent: $offsetParent})) + } + + helpBubble.jamtrackLandingVideo = function($element, $offsetParent) { + context.JK.prodBubble($element, 'jamtrack-landing-video', {}, bigHelpOptions({positions:['left'], offsetParent: $offsetParent})) + } + + helpBubble.jamtrackLandingCta = function($element, $alternativeElement) { + if ($element.visible()) { + context.JK.prodBubble($element, 'jamtrack-landing-cta', {}, bigHelpOptions({positions:['left']})) + } + else { + context.JK.prodBubble($alternativeElement, 'jamtrack-landing-cta', {}, bigHelpOptions({positions:['right']})) + } + } + + helpBubble.jamtrackBrowseBand = function($element, $offsetParent) { + return context.JK.prodBubble($element, 'jamtrack-browse-band', {}, bigHelpOptions({positions:['top'], offsetParent: $offsetParent})) + } + + helpBubble.jamtrackBrowseMasterMix = function($element, $offsetParent) { + return context.JK.prodBubble($element, 'jamtrack-browse-master-mix', {}, bigHelpOptions({positions:['top'], offsetParent: $offsetParent})) + } + + helpBubble.jamtrackBrowseCta = function($element, $offsetParent) { + return context.JK.prodBubble($element, 'jamtrack-browse-cta', {}, bigHelpOptions({positions:['top'], offsetParent: $offsetParent})) + } + })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index 215e34124..6e740f8fa 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -499,6 +499,80 @@ return profile; } + function createAffiliatePartner(options) { + return $.ajax({ + type: "POST", + url: '/api/affiliate_partners', + dataType: "json", + contentType: 'application/json', + data: JSON.stringify(options) + }) + } + + function getAffiliatePartnerData(userId) { + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/users/"+userId+"/affiliate_partner" + }); + } + + function postAffiliatePartnerData(userId, data) { + return $.ajax({ + type: "POST", + dataType: "json", + url: "/api/users/"+userId+"/affiliate_partner", + contentType: 'application/json', + processData:false, + data: JSON.stringify(data) + }); + } + + function getLinks(type, partner_id) { + var url = "/api/links/" + type; + + if(partner_id) { + url += '?affiliate_id=' + partner_id; + } + return $.ajax({ + type: "GET", + dataType: "json", + url: url + }); + } + + function getAffiliateSignups() { + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/affiliate_partners/signups" + }); + } + + function getAffiliateMonthly() { + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/affiliate_partners/monthly_earnings" + }); + } + + function getAffiliateQuarterly() { + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/affiliate_partners/quarterly_earnings" + }); + } + + function getAffiliatePayments() { + return $.ajax({ + type: "GET", + dataType: "json", + url: "/api/affiliate_partners/payments" + }); + } + function getCities(options) { var country = options['country'] var region = options['region'] @@ -1492,12 +1566,13 @@ function enqueueJamTrack(options) { var jamTrackId = options['id']; + delete options['id'] return $.ajax({ type: "POST", - url: '/api/jamtracks/enqueue/' + jamTrackId + '?' + $.param(options), + url: '/api/jamtracks/enqueue/' + jamTrackId, dataType: "json", - contentType: 'applications/json' + data: options }); } @@ -1738,6 +1813,14 @@ this.updateScheduledSession = updateScheduledSession; this.getUserDetail = getUserDetail; this.getUserProfile = getUserProfile; + this.getAffiliatePartnerData = getAffiliatePartnerData; + this.postAffiliatePartnerData = postAffiliatePartnerData; + this.createAffiliatePartner = createAffiliatePartner; + this.getLinks = getLinks; + this.getAffiliateSignups = getAffiliateSignups; + this.getAffiliateMonthly = getAffiliateMonthly; + this.getAffiliateQuarterly = getAffiliateQuarterly; + this.getAffiliatePayments = getAffiliatePayments; this.getCities = getCities; this.getRegions = getRegions; this.getCountries = getCountries; @@ -1882,4 +1965,4 @@ }; -})(window,jQuery); \ No newline at end of file +})(window,jQuery); diff --git a/web/app/assets/javascripts/jam_track_preview.js.coffee b/web/app/assets/javascripts/jam_track_preview.js.coffee index a02a45896..f78a25d05 100644 --- a/web/app/assets/javascripts/jam_track_preview.js.coffee +++ b/web/app/assets/javascripts/jam_track_preview.js.coffee @@ -9,7 +9,7 @@ context.JK.JamTrackPreview = class JamTrackPreview @EVENTS = context.JK.EVENTS @rest = context.JK.Rest() @logger = context.JK.logger - @options = options || {master_shows_duration: false, color:'gray', add_line_break: false} + @options = options || {master_shows_duration: false, color:'gray', add_line_break: false, preload_master:false} @app = app @jamTrack = jamTrack @jamTrackTrack = jamTrackTrack @@ -19,7 +19,9 @@ context.JK.JamTrackPreview = class JamTrackPreview @instrumentIcon = null @instrumentName = null @part = null + @loaded = false @loading = null + @loadingText = null template = $('#template-jam-track-preview') throw "no jam track preview template" if not template.exists() @@ -31,6 +33,7 @@ context.JK.JamTrackPreview = class JamTrackPreview @instrumentName = @root.find('.instrument-name') @part = @root.find('.part') @loading = @root.find('.loading') + @loadingText = @root.find('.loading-text') @playButton.on('click', @play) @stopButton.on('click', @stop) @@ -91,6 +94,16 @@ context.JK.JamTrackPreview = class JamTrackPreview if @no_audio @playButton.addClass('disabled') @stopButton.addClass('disabled') + else + if @options.preload_master && @jamTrackTrack.track_type == 'Master' + @sound = new Howl({ + src: @urls, + autoplay: false, + loop: false, + volume: 1.0, + preload: true, + onload: @onHowlerLoad + onend: @onHowlerEnd}) onDestroyed: () => @sound.unload() @@ -108,12 +121,16 @@ context.JK.JamTrackPreview = class JamTrackPreview @removeNowPlaying() onHowlerLoad: () => - @loading.addClass('hidden') + @loaded = true + @loading.fadeOut(); + @loadingText.fadeOut(); #addClass('hidden') play: (e) => if e? e.stopPropagation() + $(this).triggerHandler(@EVENTS.PREVIEW_PLAYED) + if @no_audio context.JK.prodBubble(@playButton, 'There is no preview available for this track.', {}, {duration:2000}) else @@ -129,7 +146,9 @@ context.JK.JamTrackPreview = class JamTrackPreview onload: @onHowlerLoad onend: @onHowlerEnd}) + unless @loaded @loading.removeClass('hidden') + @loadingText.removeClass('hidden') @logger.debug("play issued for jam track preview") diff --git a/web/app/assets/javascripts/jam_track_screen.js.coffee b/web/app/assets/javascripts/jam_track_screen.js.coffee index 43411b93b..ac25cc4c8 100644 --- a/web/app/assets/javascripts/jam_track_screen.js.coffee +++ b/web/app/assets/javascripts/jam_track_screen.js.coffee @@ -6,7 +6,8 @@ context.JK.JamTrackScreen=class JamTrackScreen LIMIT = 10 instrument_logo_map = context.JK.getInstrumentIconMap24() - constructor: (@app) -> + constructor: (@app) -> + @EVENTS = context.JK.EVENTS @logger = context.JK.logger @screen = null @content = null @@ -21,6 +22,7 @@ context.JK.JamTrackScreen=class JamTrackScreen @next = null @currentQuery = this.defaultQuery() @expanded = null + @shownHelperBubbles = false beforeShow:(data) => this.setFilterFromURL() @@ -38,11 +40,17 @@ context.JK.JamTrackScreen=class JamTrackScreen ) else this.refresh() + unless @shownHelperBubbles + @shownHelperBubbles = true + @startHelperBubbles() afterShow:(data) => context.JK.Tracking.jamtrackBrowseTrack(@app) beforeHide: () => + this.clearCtaHelpTimeout() + this.clearBandFilterHelpTimeout() + this.clearMasterHelpTimeout() this.clearResults(); events:() => @@ -57,6 +65,104 @@ context.JK.JamTrackScreen=class JamTrackScreen @noMoreJamtracks.hide() @next = null + startHelperBubbles: () => + @showBandFilterHelpTimeout = setTimeout(@showBandFilterHelp, 3500) + + showBandFilterHelp: () => + context.JK.HelpBubbleHelper.jamtrackBrowseBand(@artist.closest('.easydropdown-wrapper'), $('body')) + + @showMasterHelpDueTime = new Date().getTime() + 11000 # 6000 ms for band tooltip to display, and 5 seconds of quiet time + @scroller.on('scroll', @masterHelpScrollWatch) + @scroller.on('scroll', @clearBubbles) + @showMasterHelpTimeout = setTimeout(@showMasterHelp, @masterHelpDueTime()) + + clearBubbles: () => + if @helpBubble? + @helpBubble.btOff() + @helpBubble = null + + # computes when we should show the master help bubble + masterHelpDueTime: () => + dueTime = @showMasterHelpDueTime - new Date().getTime() + if dueTime <= 0 + dueTime = 2000 + dueTime + + + # computes when we should show the master help bubble + ctaHelpDueTime: () => + dueTime = @showCtaHelpDueTime - new Date().getTime() + if dueTime <= 0 + dueTime = 2000 + dueTime + + # if the user scrolls, reset the master help due time + masterHelpScrollWatch: () => + @clearMasterHelpTimeout() + @showMasterHelpTimeout = setTimeout(@showMasterHelp, @masterHelpDueTime() + 2000) + + # if the user scrolls, reset the master help due time + ctaHelpScrollWatch: () => + @clearCtaHelpTimeout() + @showCtaHelpTimeout = setTimeout(@showCtaHelp, @ctaHelpDueTime() + 2000) + + + showCtaHelp: () => + @scroller.off('scroll', @ctaHelpScrollWatch) + @clearCtaHelpTimeout() + + cutoff = @scroller.offset().top; + + @screen.find('.jamtrack-actions').each((i, element) => + $element = $(element) + + if ($element.offset().top >= cutoff) + @helpBubble = context.JK.HelpBubbleHelper.jamtrackBrowseCta($element, $('body')) + return false + else + return true + ) + + showMasterHelp: () => + @scroller.off('scroll', @masterHelpScrollWatch) + @clearMasterHelpTimeout() + + # don't show the help if the user has already clicked a preview + unless @userPreviewed + cutoff = @scroller.offset().top; + + @screen.find('.jamtrack-preview[data-track-type="Master"]').each((i, element) => + $element = $(element) + + if ($element.offset().top >= cutoff) + @helpBubble = context.JK.HelpBubbleHelper.jamtrackBrowseMasterMix($element.find('.play-button'), $('body')) + return false + else + return true + ) + + @showCtaHelpDueTime = new Date().getTime() + 11000 + @scroller.on('scroll', @ctaHelpScrollWatch) + @showCtaHelpTimeout = setTimeout(@showCtaHelp, @ctaHelpDueTime()) # 6000 ms for bubble show time, and 5000ms for delay + + previewPlayed: () => + @userPreviewed = true + + clearCtaHelpTimeout:() => + if @showCtaHelpTimeout? + clearTimeout(@showCtaHelpTimeout) + @showCtaHelpTimeout = null + + clearBandFilterHelpTimeout: () => + if @showBandFilterHelpTimeout? + clearTimeout(@showBandFilterHelpTimeout) + @showBandFilterHelpTimeout = null + + clearMasterHelpTimeout: () => + if @showMasterHelpTimeout? + clearTimeout(@showMasterHelpTimeout) + @showMasterHelpTimeout = null + setFilterFromURL:() => # Grab parms from URL for artist, instrument, and availability parms=this.getParams() @@ -73,7 +179,9 @@ context.JK.JamTrackScreen=class JamTrackScreen @availability.val(parms.availability) else @availability.val('') - window.history.replaceState({}, "", "/client#/jamtrackBrowse") + + if window.history.replaceState #ie9 proofing + window.history.replaceState({}, "", "/client#/jamtrackBrowse") getParams:() => params = {} @@ -320,7 +428,8 @@ context.JK.JamTrackScreen=class JamTrackScreen for track in jamTrack.tracks trackRow = jamtrackElement.find("[jamtrack-track-id='#{track.id}']") previewElement = trackRow.find(".jamtrack-preview") - new JK.JamTrackPreview(@app, previewElement, jamTrack, track, {master_shows_duration: true, color:'gray'}) + preview = new JK.JamTrackPreview(@app, previewElement, jamTrack, track, {master_shows_duration: true, color:'gray'}) + $(preview).on(@EVENTS.PREVIEW_PLAYED, @previewPlayed) this.handleExpanded(jamtrackElement, false) diff --git a/web/app/assets/javascripts/jamtrack_landing.js.coffee b/web/app/assets/javascripts/jamtrack_landing.js.coffee index 2fb470923..5198a1f64 100644 --- a/web/app/assets/javascripts/jamtrack_landing.js.coffee +++ b/web/app/assets/javascripts/jamtrack_landing.js.coffee @@ -72,7 +72,8 @@ context.JK.JamTrackLanding = class JamTrackLanding that=this @bandList.on "click", "a.artist-link", (event)-> context.location="client#/jamtrackBrowse" - window.history.replaceState({}, "", this.href) + if window.history.replaceState # ie9 proofing + window.history.replaceState({}, "", this.href) event.preventDefault() handleFailure:(error) => diff --git a/web/app/assets/javascripts/landing/landing.js b/web/app/assets/javascripts/landing/landing.js index 19034e53c..de23401db 100644 --- a/web/app/assets/javascripts/landing/landing.js +++ b/web/app/assets/javascripts/landing/landing.js @@ -1,3 +1,4 @@ +//= require bugsnag //= require bind-polyfill //= require jquery //= require jquery.monkeypatch diff --git a/web/app/assets/javascripts/landing/signup.js b/web/app/assets/javascripts/landing/signup.js index a91329222..0eec67cf4 100644 --- a/web/app/assets/javascripts/landing/signup.js +++ b/web/app/assets/javascripts/landing/signup.js @@ -156,6 +156,9 @@ submit_data.city = $('#jam_ruby_user_city').val() submit_data.birth_date = gather_birth_date() submit_data.instruments = gather_instruments() + if($.QueryString['affiliate_partner_id']) { + submit_data.affiliate_partner_id = $.QueryString['affiliate_partner_id']; + } //submit_data.photo_url = $('#jam_ruby_user_instruments').val() diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index db0826c0f..8af882715 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -630,6 +630,10 @@ return (suppressDay ? '' : (days[date.getDay()] + ' ')) + months[date.getMonth()] + ' ' + context.JK.padString(date.getDate(), 2) + ', ' + date.getFullYear(); } + // returns June for months 0-11 + context.JK.getMonth = function(monthNumber) { + return months[monthNumber]; + } context.JK.formatDateYYYYMMDD = function(dateString) { var date = new Date(dateString); diff --git a/web/app/assets/javascripts/web/affiliate_links.js.coffee b/web/app/assets/javascripts/web/affiliate_links.js.coffee new file mode 100644 index 000000000..be15b3e5a --- /dev/null +++ b/web/app/assets/javascripts/web/affiliate_links.js.coffee @@ -0,0 +1,55 @@ + + +$ = jQuery +context = window +context.JK ||= {}; + +class AffiliateLinks + constructor: (@app, @partner_id) -> + @logger = context.JK.logger + @rest = new context.JK.Rest(); + + initialize: () => + @page = $('body') + @sections = ['jamtrack_songs', 'jamtrack_bands', 'jamtrack_general', 'jamkazam', 'sessions', 'recordings'] + @jamtrack_songs = @page.find('table.jamtrack_songs tbody') + @jamtrack_bands = @page.find('table.jamtrack_bands tbody') + @jamtrack_general = @page.find('table.jamtrack_general tbody') + @jamkazam = @page.find('table.jamkazam tbody') + @sessions= @page.find('table.sessions tbody') + @recordings = @page.find('table.recordings tbody') + + @iterate() + + onGetLinks: (links) => + + $table = @page.find('table.' + @section + ' tbody') + template = $('#template-affiliate-link-row').html(); + context._.each(links, (item) => + $link = $(context._.template(template, item, {variable: 'data'})); + $link.find('td.copy-link a').click(@copyLink) + $table.append($link) + ) + + if @sections.length > 0 + @iterate() + + + copyLink: () -> + $element = $(this) + $url = $element.closest('tr').find('td.url input') + $url.select() + + return false; + + + iterate: () => + @section = @sections.shift() + + @rest.getLinks(@section, @partner_id) + .done(@onGetLinks) + + + +context.JK.AffiliateLinks = AffiliateLinks + diff --git a/web/app/assets/javascripts/web/affiliate_program.js.coffee b/web/app/assets/javascripts/web/affiliate_program.js.coffee new file mode 100644 index 000000000..c013abbf2 --- /dev/null +++ b/web/app/assets/javascripts/web/affiliate_program.js.coffee @@ -0,0 +1,119 @@ + + +$ = jQuery +context = window +context.JK ||= {}; + +class AffiliateProgram + constructor: (@app) -> + @logger = context.JK.logger + @rest = new context.JK.Rest(); + @agreeBtn = null + @disagreeBtn = null + @entityForm = null + @disagreeNotice = null + @entityName = null + @entityType = null + @entityRadio = null + @fieldEntityName = null + @fieldEntityType = null + @entityOptions = null + + removeErrors: () => + @fieldEntityName.removeClass('error').find('.error-info').remove(); + @fieldEntityType.removeClass('error').find('.error-info').remove(); + @entityOptions.removeClass('error').find('.error-info').remove(); + + onRadioChanged: () => + @removeErrors() + value = @page.find('input:radio[name="entity"]:checked').val() + + if value == 'individual' + @entityForm.slideUp() + else + @entityForm.slideDown() + + return false + + + onCreatedAffiliatePartner:(response) => + if response.partner_user_id? + # this was an existing user, so tell them to go on in + context.JK.Banner.show({buttons: [{name: 'GO TO AFFILIATE PAGE', href: '/client#/account/affiliatePartner'}], title: 'congratulations', html: 'Thank you for joining the JamKazam affiliate program!

    You can visit the Affiliate Page in your JamKazam Account any time to get links to share to refer users, and to view reports on affiliate activity levels.'}) + else + context.JK.Banner.show({buttons: [{name: 'GO SIGNUP', href:'/signup?affiliate_partner_id=' + response.id}], title: 'congratulations', html: 'Thank you for joining the JamKazam affiliate program!

    There is still one more step: you still need to create a user account on JamKazam, so that you can access your affiliate information.'}) + + onFailedCreateAffiliatePartner: (jqXHR) => + if jqXHR.status == 422 + body = JSON.parse(jqXHR.responseText) + if body.errors && body.errors.affiliate_partner && body.errors.affiliate_partner[0] == 'You are already an affiliate.' + @app.notify({title:'Error', text:'You are already an affiliate.'}) + else + @app.notifyServerError(jqXHR, 'Unable to Create Affiliate') + else + @app.notifyServerError(jqXHR, 'Unable to Create Affiliate') + + onAgreeClicked: () => + @removeErrors() + + value = @page.find('input:radio[name="entity"]:checked').val() + + error = false + + if value? + if value == 'individual' + entityType = 'Individual' + else + # insist that they fill out entity type info + entityName = @entityName.val() + entityType = @entityType.val() + + entityNameNotEmpty = !!entityName + entityTypeNotEmpty = !!entityType + + if !entityNameNotEmpty + @fieldEntityName.addClass('error').append('

    must be specified
    ') + error = true + + if !entityTypeNotEmpty + @fieldEntityType.addClass('error').append('
    must be specified
    ') + error = true + else + @entityOptions.addClass('error') + error = true + + unless error + @rest.createAffiliatePartner({partner_name: entityName, entity_type: entityType}) + .done(@onCreatedAffiliatePartner) + .fail(@onFailedCreateAffiliatePartner) + + @disagreeNotice.hide ('hidden') + return false + + onDisagreeClicked: () => + @removeErrors() + + @disagreeNotice.slideDown('hidden') + return false + + events:() => + @entityRadio.on('change', @onRadioChanged) + @agreeBtn.on('click', @onAgreeClicked) + @disagreeBtn.on('click', @onDisagreeClicked) + + initialize: () => + @page = $('body') + @agreeBtn = @page.find('.agree-button') + @disagreeBtn = @page.find('.disagree-button') + @entityForm = @page.find('.entity-info') + @disagreeNotice = @page.find('.disagree-text') + @entityName = @page.find('input[name="entity-name"]') + @entityType = @page.find('select[name="entity-type"]') + @entityRadio = @page.find('input[name="entity"]') + @fieldEntityName = @page.find('.field.entity.name') + @fieldEntityType = @page.find('.field.entity.type') + @entityOptions = @page.find('.entity-options') + + @events() + +context.JK.AffiliateProgram = AffiliateProgram \ No newline at end of file diff --git a/web/app/assets/javascripts/web/individual_jamtrack.js b/web/app/assets/javascripts/web/individual_jamtrack.js index 9eff40023..ebb720f2c 100644 --- a/web/app/assets/javascripts/web/individual_jamtrack.js +++ b/web/app/assets/javascripts/web/individual_jamtrack.js @@ -44,12 +44,14 @@ $previews.append($element); - new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false, color:'black', master_adds_line_break: true}) + new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false, color:'black', master_adds_line_break: true, preload_master:true}) + + if(track.track_type =='Master') { + context.JK.HelpBubbleHelper.rotateJamTrackLandingBubbles($element.find('.jam-track-preview'), $page.find('.video-wrapper'), $page.find('.cta-free-jamtrack a'), $page.find('a.browse-jamtracks')); + } }) $previews.append('
    ') - - }) .fail(function () { app.notify({title: 'Unable to fetch JamTrack', text: "Please refresh the page or try again later."}) diff --git a/web/app/assets/javascripts/web/individual_jamtrack_band.js b/web/app/assets/javascripts/web/individual_jamtrack_band.js index c05f58772..22ceb52a3 100644 --- a/web/app/assets/javascripts/web/individual_jamtrack_band.js +++ b/web/app/assets/javascripts/web/individual_jamtrack_band.js @@ -34,7 +34,11 @@ $previews.append($element); - new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false, color:'black', master_adds_line_break:true}) + new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false, color:'black', master_adds_line_break:true, preload_master:true}) + + if(track.track_type =='Master') { + context.JK.HelpBubbleHelper.rotateJamTrackLandingBubbles($element.find('.jam-track-preview'), $page.find('.video-wrapper'), $page.find('.cta-free-jamtrack a'), $page.find('a.browse-jamtracks')); + } }) $previews.append('
    ') diff --git a/web/app/assets/javascripts/web/web.js b/web/app/assets/javascripts/web/web.js index 408eaf41e..89f49d6fe 100644 --- a/web/app/assets/javascripts/web/web.js +++ b/web/app/assets/javascripts/web/web.js @@ -1,3 +1,4 @@ +//= require bugsnag //= require bind-polyfill //= require jquery //= require jquery.monkeypatch @@ -20,6 +21,7 @@ //= require jquery.icheck //= require jquery.bt //= require jquery.exists +//= require jquery.visible //= require howler.core.js //= require AAA_Log //= require AAC_underscore @@ -65,6 +67,8 @@ //= require web/tracking //= require web/individual_jamtrack //= require web/individual_jamtrack_band +//= require web/affiliate_program +//= require web/affiliate_links //= require fakeJamClient //= require fakeJamClientMessages //= require fakeJamClientRecordings diff --git a/web/app/assets/javascripts/wizard/gear/step_select_gear.js b/web/app/assets/javascripts/wizard/gear/step_select_gear.js index eda4bbe9b..88f07f295 100644 --- a/web/app/assets/javascripts/wizard/gear/step_select_gear.js +++ b/web/app/assets/javascripts/wizard/gear/step_select_gear.js @@ -884,7 +884,11 @@ // deal with sample rate if(selectedDeviceInfo && context.jamClient.FTUESetPreferredMixerSampleRate) { // get the preferred sample rate for the device, and set that as the initial value of the sample rate dropdown - if (selectedDeviceInfo.input.info.port_audio_name != 'Default Input') { + // except for WKS; we just default to 48 + if (selectedDeviceInfo.input.info.type == 'Win32_wdm') { + var inputSampleRate = 48000; + } + else if (selectedDeviceInfo.input.info.port_audio_name != 'Default Input') { var inputSampleRate = context.jamClient.FTUEgetInputDeviceSampleRate(selectedDeviceInfo.input.info.port_audio_name); } else { diff --git a/web/app/assets/javascripts/wizard/gear_utils.js b/web/app/assets/javascripts/wizard/gear_utils.js index 193bc05ed..d50238724 100644 --- a/web/app/assets/javascripts/wizard/gear_utils.js +++ b/web/app/assets/javascripts/wizard/gear_utils.js @@ -197,7 +197,7 @@ } var devices = context.jamClient.FTUEGetAudioDevices(); - logger.debug("FTUEGetAudioDevices: " + JSON.stringify(devices)); + //logger.debug("FTUEGetAudioDevices: " + JSON.stringify(devices)); var loadedDevices = {}; diff --git a/web/app/assets/stylesheets/client/account.css.scss b/web/app/assets/stylesheets/client/account.css.scss index d37147db9..fae2ccd46 100644 --- a/web/app/assets/stylesheets/client/account.css.scss +++ b/web/app/assets/stylesheets/client/account.css.scss @@ -21,6 +21,13 @@ } } + hr { + height:0; + border-width: 1px 0 0 0; + border-style:solid; + border-color:$ColorTextTypical; + } + h4 { margin-top:8px; margin-bottom: 10px; @@ -57,7 +64,7 @@ .subcaption { margin-bottom: 4px; - } + } } .webcam-container { diff --git a/web/app/assets/stylesheets/client/account_affiliate.css.scss b/web/app/assets/stylesheets/client/account_affiliate.css.scss new file mode 100644 index 000000000..e1e36f203 --- /dev/null +++ b/web/app/assets/stylesheets/client/account_affiliate.css.scss @@ -0,0 +1,201 @@ +@import 'common.css.scss'; + +#account-affiliate-partner { + + + p { + font-size: 15px; + line-height: 125%; + margin:0; + } + + .affiliates-header { + float:left; + } + + .affiliate-partner-nav { + width:85%; + position:relative; + float:right; + margin-bottom:20px; + } + + .affiliate-partner-nav a { + width:19%; + text-align:center; + height: 27px; + display: block; + float:right; + margin-right:5px; + vertical-align:bottom; + padding-top:10px; + background-color:#535353; + color:#ccc; + font-size:17px; + text-decoration:none; + } + + .affiliate-partner-nav a:hover { + background-color:#666; + color:#fff; + } + + .affiliate-partner-nav a.active { + background-color:#ed3618; + color:#fff; + } + + .affiliate-partner-nav a.active:hover { + background-color:#ed3618; + cursor:default; + } + + .affiliate-partner-nav a.last { + margin-right:0px !important; + } + + #affiliate-partner-tab-content { + .tab-account { + .left-col { + float: left; + width: 55%; + .affiliate-label { + text-align: left; + width: 130px; + float: left; + display: inline-block; + margin-right: 10px; + margin-top: 3px; + } + input { + margin-bottom: 5px; + @include border_box_sizing; + } + + .button-orange { + margin-right:2px; + } + .spacer { + width:140px; + display:inline-block; + } + .input-buttons { + width:60%; + display:inline-block; + } + } + .right-col { + float: right; + width: 45%; + margin-top: 30px; + line-height:125%; + } + } + } + + .links { + p.prompt { + margin-bottom:20px; + } + } + + .link-type-section { + border-width:0 0 1px 0; + border-style:solid; + border-color:$ColorTextTypical; + padding-bottom:20px; + margin-bottom:20px; + + label { + display:inline-block; + margin-right:20px; + } + select { + + } + } + + .link-type-prompt { + display:none; + + p { + margin-bottom:20px; + } + + .example-link { + font-weight:bold; + } + } + + table.traffic-table { + width:400px; + .signups, .visits { + width:70px; + text-align:right; + } + } + table.links-table { + min-width:100%; + margin-top:20px; + + th { + padding: 10px 0 20px; + white-space:nowrap; + } + + td { + padding:3px 0; + white-space:nowrap; + } + .target { + width:45%; + white-space:normal; + } + .copy-link { + width:100px; + } + + .url { + + input { + background-color: transparent; + -webkit-box-shadow:none; + box-shadow:none; + color:#ccc; + } + } + } + + .agreement { + #partner-agreement-v1 { + height:340px; + overflow:scroll; + padding:20px; + border:1px solid white; + @include border-box_sizing; + } + + label.partner-agreement { + display:inline-block; + margin-right:40px; + } + + h2 { + font-size:20px; + font-weight:bold; + margin:40px 30px 20px 0; + display:inline-block; + } + + .execution-date { + display:inline-block; + } + + .input-buttons { + margin-top:20px; + } + } + + + +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index 3296155e8..0e0bee1f1 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -9,6 +9,7 @@ * compiled file, but it's generally better to create a new file per style scope. * *= require_self + *= require web/Raleway *= require jquery.ui.datepicker *= require ./ie *= require jquery.bt @@ -35,6 +36,7 @@ *= require ./accountProfileInterests *= require ./accountProfileSamples *= require ./accountPaymentHistory + *= require ./account_affiliate *= require ./search *= require ./ftue *= require ./jamServer @@ -81,4 +83,5 @@ *= require ./downloadJamTrack *= require ./jamTrackPreview *= require users/signinCommon + *= require landings/partner_agreement_v1 */ \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/help.css.scss b/web/app/assets/stylesheets/client/help.css.scss index b5c3cf17b..1bd085fa4 100644 --- a/web/app/assets/stylesheets/client/help.css.scss +++ b/web/app/assets/stylesheets/client/help.css.scss @@ -50,6 +50,8 @@ body.jam, body.web, .dialog{ .big-help { font-size:20px; + color: #ed3618; + p {color:#ed3618} } .help-hover-recorded-tracks, .help-hover-stream-mix, .help-hover-recorded-backing-tracks { diff --git a/web/app/assets/stylesheets/client/jamTrackPreview.css.scss b/web/app/assets/stylesheets/client/jamTrackPreview.css.scss index 37b871e77..c3cae6e81 100644 --- a/web/app/assets/stylesheets/client/jamTrackPreview.css.scss +++ b/web/app/assets/stylesheets/client/jamTrackPreview.css.scss @@ -69,6 +69,19 @@ } } + .loading-text { + position:absolute; + right:-135px; + top:0; + padding:0 3px; + font-style:italic; + z-index:1; + -webkit-border-radius:10px; + -moz-border-radius:10px; + border-radius:10px; + border:0 solid black; + } + .adds-line-break { display:block; margin-left:66px; diff --git a/web/app/assets/stylesheets/client/jamkazam.css.scss b/web/app/assets/stylesheets/client/jamkazam.css.scss index 105946377..af98aad9a 100644 --- a/web/app/assets/stylesheets/client/jamkazam.css.scss +++ b/web/app/assets/stylesheets/client/jamkazam.css.scss @@ -24,11 +24,11 @@ body { height: 100%; overflow: hidden; font-size: 14px; - font-family: Raleway, Arial, Helvetica, sans-serif; + font-family: 'Raleway', Arial, Helvetica, sans-serif; font-weight: 300; input,textarea { - font-family: Raleway, Arial, Helvetica, sans-serif; + font-family: 'Raleway', Arial, Helvetica, sans-serif; font-weight: 300; } } @@ -556,22 +556,22 @@ hr { } ::-webkit-input-placeholder { - font-family: Raleway, Arial, Helvetica, sans-serif; + font-family: 'Raleway', Arial, Helvetica, sans-serif; font-size:14px; } :-moz-placeholder { /* Firefox 18- */ - font-family: Raleway, Arial, Helvetica, sans-serif; + font-family: 'Raleway', Arial, Helvetica, sans-serif; font-size:14px; } ::-moz-placeholder { /* Firefox 19+ */ - font-family: Raleway, Arial, Helvetica, sans-serif; + font-family: 'Raleway', Arial, Helvetica, sans-serif; font-size:14px; } :-ms-input-placeholder { - font-family: Raleway, Arial, Helvetica, sans-serif; + font-family: 'Raleway', Arial, Helvetica, sans-serif; font-size:14px; } diff --git a/web/app/assets/stylesheets/client/jamtrack.css.scss b/web/app/assets/stylesheets/client/jamtrack.css.scss index 0bbce8be2..7b3a27aaf 100644 --- a/web/app/assets/stylesheets/client/jamtrack.css.scss +++ b/web/app/assets/stylesheets/client/jamtrack.css.scss @@ -198,20 +198,21 @@ .jam-track-preview { font-size:11px; white-space:nowrap; + + .loading-text { + right:-115px; + background-color:#262626; + } } .jamtrack-action { @include border_box_sizing; width: 20%; text-align: center; + vertical-align: middle; .jamtrack-action-container { - display: flex; - align-items: center; - justify-content: center; - position:absolute; - height:100%; - width:100%; + display:inline-block; } .play-button { diff --git a/web/app/assets/stylesheets/client/metronomePlaybackModeSelect.css.scss b/web/app/assets/stylesheets/client/metronomePlaybackModeSelect.css.scss index 7700a57be..79d52c274 100644 --- a/web/app/assets/stylesheets/client/metronomePlaybackModeSelect.css.scss +++ b/web/app/assets/stylesheets/client/metronomePlaybackModeSelect.css.scss @@ -7,7 +7,7 @@ overflow:auto; border:1px solid #ED3618; text-align:left; - font-family: Raleway, Arial, Helvetica, sans-serif; + font-family: 'Raleway', Arial, Helvetica, sans-serif; ul { height:100%; margin-left:20px; diff --git a/web/app/assets/stylesheets/client/muteSelect.css.scss b/web/app/assets/stylesheets/client/muteSelect.css.scss index bd73ce571..c155de69d 100644 --- a/web/app/assets/stylesheets/client/muteSelect.css.scss +++ b/web/app/assets/stylesheets/client/muteSelect.css.scss @@ -8,7 +8,7 @@ overflow:auto; border:1px solid #ED3618; text-align:center; - font-family: Raleway, Arial, Helvetica, sans-serif; + font-family: 'Raleway', Arial, Helvetica, sans-serif; ul { @include vertical-align-column; height:100%; diff --git a/web/app/assets/stylesheets/client/sessionList.css.scss b/web/app/assets/stylesheets/client/sessionList.css.scss index ce326ef9c..c39ee2fcd 100644 --- a/web/app/assets/stylesheets/client/sessionList.css.scss +++ b/web/app/assets/stylesheets/client/sessionList.css.scss @@ -1,7 +1,7 @@ @import "client/common"; -table.findsession-table, table.local-recordings, table.open-jam-tracks, table.open-backing-tracks, table.cart-items, #account-session-detail, table.payment-table { +table.findsession-table, table.local-recordings, table.open-jam-tracks, table.open-backing-tracks, table.cart-items, #account-session-detail, table.payment-table, table.jamtable { .latency-unacceptable { width: 50px; @@ -64,7 +64,7 @@ table.findsession-table, table.local-recordings, table.open-jam-tracks, table.op text-align:center; } } -table.findsession-table, table.local-recordings, table.open-jam-tracks, table.open-backing-tracks, table.cart-items, table.payment-table { +table.findsession-table, table.local-recordings, table.open-jam-tracks, table.open-backing-tracks, table.cart-items, table.payment-table, table.jamtable { width:98%; height:10%; font-size:11px; diff --git a/web/app/assets/stylesheets/corp/corporate.css.scss.erb b/web/app/assets/stylesheets/corp/corporate.css.scss.erb index 640360bd1..f7e8907f2 100644 --- a/web/app/assets/stylesheets/corp/corporate.css.scss.erb +++ b/web/app/assets/stylesheets/corp/corporate.css.scss.erb @@ -1,4 +1,5 @@ /** +*= require web/Raleway *= require client/ie *= require client/jamkazam *= require client/header diff --git a/web/app/assets/stylesheets/landing/landing.css b/web/app/assets/stylesheets/landing/landing.css index c3ce73d41..a37a24675 100644 --- a/web/app/assets/stylesheets/landing/landing.css +++ b/web/app/assets/stylesheets/landing/landing.css @@ -1,4 +1,5 @@ /** +*= require web/Raleway *= require client/ie *= require client/jamkazam *= require client/screen_common diff --git a/web/app/assets/stylesheets/landings/affiliate_program.css.scss b/web/app/assets/stylesheets/landings/affiliate_program.css.scss new file mode 100644 index 000000000..f695f5da7 --- /dev/null +++ b/web/app/assets/stylesheets/landings/affiliate_program.css.scss @@ -0,0 +1,81 @@ +@import "client/common.css.scss"; + +body.web.landing_page.full { + &.affiliate_program { + + .video-container { + width:100%; + padding-bottom:53.33%; + + } + .agree-disagree-buttons { + margin-top:30px; + } + + .agree-button { + margin-right:20px; + } + + #partner-agreement-v1 { + height:340px; + overflow:scroll; + padding:20px; + border:1px solid white; + @include border-box_sizing; + } + + .wrapper { + padding-top:20px; + } + .row { + text-align:left; + } + + h1 { + display: inline-block; + margin-bottom:20px; + } + + .column { + @include border_box_sizing; + width:50% ! important; + float:left; + + p { + margin-bottom:15px; + } + } + + label { + display:inline-block; + color:#ccc; + } + + p.agreement-notice { + color:white; + margin:20px 0 30px; + } + + .field.radio { + margin-top:10px; + label { + margin-left:10px; + } + } + + .field.entity { + &.name { + margin-top:30px; + } + margin-top:10px; + width:400px; + input, select { + width:300px; + @include border_box_sizing; + } + label { + width:100px; + } + } + } +} diff --git a/web/app/assets/stylesheets/landings/partner_agreement_v1.css.scss b/web/app/assets/stylesheets/landings/partner_agreement_v1.css.scss new file mode 100644 index 000000000..647f77f9a --- /dev/null +++ b/web/app/assets/stylesheets/landings/partner_agreement_v1.css.scss @@ -0,0 +1,4667 @@ +#partner-agreement-v1 { + + background-color:#eee; + * { + color:black! important; + background-color:#eee; + font-family: 'Raleway', Arial, Helvetica, sans-serif ! important; + font-weight: 300 ! important; + } + + .c1, .c20 { + border-bottom-color: #CCCCCC ! important; + border-top-color: #CCCCCC ! important; + border-left-color: #CCCCCC ! important; + border-right-color: #CCCCCC ! important; + } + + .c12 { + color: #CCCCCC ! important; + } + + ul { + list-style-type:none; + } + + + + + + + .lst-kix_list_39-2 > li:before { + content: "\0025aa " + } + + .lst-kix_list_30-8 > li:before { + content: "\0025aa " + } + + .lst-kix_list_22-8 > li:before { + content: "\0025cf " + } + + .lst-kix_list_6-7 > li:before { + content: "" counter(lst-ctn-kix_list_6-0, decimal) "." counter(lst-ctn-kix_list_6-1, decimal) "." counter(lst-ctn-kix_list_6-2, decimal) "." counter(lst-ctn-kix_list_6-3, decimal) "." counter(lst-ctn-kix_list_6-4, decimal) "." counter(lst-ctn-kix_list_6-5, decimal) "." counter(lst-ctn-kix_list_6-6, decimal) "." counter(lst-ctn-kix_list_6-7, decimal) " " + } + + .lst-kix_list_34-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_21-3 > li:before { + content: "" counter(lst-ctn-kix_list_21-0, decimal) "." counter(lst-ctn-kix_list_21-1, decimal) "." counter(lst-ctn-kix_list_21-2, decimal) "." counter(lst-ctn-kix_list_21-3, decimal) " " + } + + .lst-kix_list_43-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_41-8 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_29-6 { + list-style-type: none + } + + ul.lst-kix_list_29-5 { + list-style-type: none + } + + ul.lst-kix_list_29-8 { + list-style-type: none + } + + ul.lst-kix_list_29-7 { + list-style-type: none + } + + ul.lst-kix_list_29-2 { + list-style-type: none + } + + ul.lst-kix_list_29-1 { + list-style-type: none + } + + .lst-kix_list_14-7 > li { + counter-increment: lst-ctn-kix_list_14-7 + } + + ul.lst-kix_list_29-4 { + list-style-type: none + } + + ul.lst-kix_list_29-3 { + list-style-type: none + } + + .lst-kix_list_41-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_8-5 > li:before { + content: "\0025cf " + } + + .lst-kix_list_48-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_25-2 > li:before { + content: "" counter(lst-ctn-kix_list_25-0, decimal) "." counter(lst-ctn-kix_list_25-1, decimal) "." counter(lst-ctn-kix_list_25-2, decimal) " " + } + + .lst-kix_list_33-2 > li:before { + content: "\0025aa " + } + + ol.lst-kix_list_19-1.start { + counter-reset: lst-ctn-kix_list_19-1 6 + } + + .lst-kix_list_21-2 > li { + counter-increment: lst-ctn-kix_list_21-2 + } + + ol.lst-kix_list_16-4.start { + counter-reset: lst-ctn-kix_list_16-4 0 + } + + ul.lst-kix_list_29-0 { + list-style-type: none + } + + .lst-kix_list_22-1 > li:before { + content: "\0025cf " + } + + .lst-kix_list_16-7 > li { + counter-increment: lst-ctn-kix_list_16-7 + } + + .lst-kix_list_32-7 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_25-2.start { + counter-reset: lst-ctn-kix_list_25-2 0 + } + + .lst-kix_list_18-0 > li:before { + content: "" counter(lst-ctn-kix_list_18-0, decimal) " " + } + + .lst-kix_list_38-4 > li:before { + content: "o " + } + + .lst-kix_list_13-4 > li:before { + content: "\0025cf " + } + + .lst-kix_list_5-2 > li:before { + content: "\0025cf " + } + + .lst-kix_list_9-1 > li:before { + content: "\0025cf " + } + + .lst-kix_list_18-0 > li { + counter-increment: lst-ctn-kix_list_18-0 + } + + .lst-kix_list_15-8 > li:before { + content: "" counter(lst-ctn-kix_list_15-0, decimal) "." counter(lst-ctn-kix_list_15-1, decimal) "." counter(lst-ctn-kix_list_15-2, decimal) "." counter(lst-ctn-kix_list_15-3, decimal) "." counter(lst-ctn-kix_list_15-4, decimal) "." counter(lst-ctn-kix_list_15-5, decimal) "." counter(lst-ctn-kix_list_15-6, decimal) "." counter(lst-ctn-kix_list_15-7, decimal) "." counter(lst-ctn-kix_list_15-8, decimal) " " + } + + .lst-kix_list_7-2 > li:before { + content: "\0025cf " + } + + .lst-kix_list_21-0 > li { + counter-increment: lst-ctn-kix_list_21-0 + } + + .lst-kix_list_37-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_37-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_17-8 > li { + counter-increment: lst-ctn-kix_list_17-8 + } + + .lst-kix_list_40-2 > li:before { + content: "\0025aa " + } + + ol.lst-kix_list_18-3.start { + counter-reset: lst-ctn-kix_list_18-3 0 + } + + .lst-kix_list_19-8 > li { + counter-increment: lst-ctn-kix_list_19-8 + } + + .lst-kix_list_31-7 > li:before { + content: "o " + } + + .lst-kix_list_36-4 > li:before { + content: "o " + } + + .lst-kix_list_9-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_6-0 > li { + counter-increment: lst-ctn-kix_list_6-0 + } + + ol.lst-kix_list_11-0 { + list-style-type: none + } + + .lst-kix_list_9-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_2-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_42-1 > li { + counter-increment: lst-ctn-kix_list_42-1 + } + + .lst-kix_list_28-7 > li:before { + content: "o " + } + + .lst-kix_list_2-4 > li:before { + content: "\0025cf " + } + + .lst-kix_list_45-5 > li:before { + content: "\0025aa " + } + + ol.lst-kix_list_15-6 { + list-style-type: none + } + + ol.lst-kix_list_15-7 { + list-style-type: none + } + + ol.lst-kix_list_15-8 { + list-style-type: none + } + + .lst-kix_list_37-4 > li:before { + content: "o " + } + + ol.lst-kix_list_15-0 { + list-style-type: none + } + + .lst-kix_list_12-7 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_15-1 { + list-style-type: none + } + + ol.lst-kix_list_15-2 { + list-style-type: none + } + + ol.lst-kix_list_15-3 { + list-style-type: none + } + + ol.lst-kix_list_15-4 { + list-style-type: none + } + + ol.lst-kix_list_15-5 { + list-style-type: none + } + + .lst-kix_list_21-4 > li:before { + content: "" counter(lst-ctn-kix_list_21-0, decimal) "." counter(lst-ctn-kix_list_21-1, decimal) "." counter(lst-ctn-kix_list_21-2, decimal) "." counter(lst-ctn-kix_list_21-3, decimal) "." counter(lst-ctn-kix_list_21-4, decimal) " " + } + + .lst-kix_list_41-2 > li:before { + content: "\0025aa " + } + + .lst-kix_list_34-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_35-7 > li:before { + content: "o " + } + + ol.lst-kix_list_17-3.start { + counter-reset: lst-ctn-kix_list_17-3 0 + } + + ol.lst-kix_list_21-6 { + list-style-type: none + } + + ol.lst-kix_list_21-5 { + list-style-type: none + } + + ol.lst-kix_list_21-4 { + list-style-type: none + } + + ol.lst-kix_list_21-3 { + list-style-type: none + } + + ol.lst-kix_list_6-5.start { + counter-reset: lst-ctn-kix_list_6-5 0 + } + + ol.lst-kix_list_21-8 { + list-style-type: none + } + + .lst-kix_list_8-1 > li:before { + content: "\0025cf " + } + + .lst-kix_list_31-2 > li:before { + content: "\0025aa " + } + + ol.lst-kix_list_21-7 { + list-style-type: none + } + + ol.lst-kix_list_8-0 { + list-style-type: none + } + + .lst-kix_list_17-6 > li { + counter-increment: lst-ctn-kix_list_17-6 + } + + .lst-kix_list_46-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_18-3 > li:before { + content: "" counter(lst-ctn-kix_list_18-0, decimal) "." counter(lst-ctn-kix_list_18-1, decimal) "." counter(lst-ctn-kix_list_18-2, decimal) "." counter(lst-ctn-kix_list_18-3, decimal) " " + } + + .lst-kix_list_41-7 > li:before { + content: "o " + } + + ol.lst-kix_list_21-1 { + list-style-type: none + } + + ol.lst-kix_list_21-2 { + list-style-type: none + } + + ol.lst-kix_list_21-0 { + list-style-type: none + } + + .lst-kix_list_33-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_46-7 > li:before { + content: "o " + } + + .lst-kix_list_25-7 > li:before { + content: "" counter(lst-ctn-kix_list_25-0, decimal) "." counter(lst-ctn-kix_list_25-1, decimal) "." counter(lst-ctn-kix_list_25-2, decimal) "." counter(lst-ctn-kix_list_25-3, decimal) "." counter(lst-ctn-kix_list_25-4, decimal) "." counter(lst-ctn-kix_list_25-5, decimal) "." counter(lst-ctn-kix_list_25-6, decimal) "." counter(lst-ctn-kix_list_25-7, decimal) " " + } + + .lst-kix_list_45-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_4-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_27-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_16-8 > li:before { + content: "" counter(lst-ctn-kix_list_16-0, decimal) "." counter(lst-ctn-kix_list_16-1, decimal) "." counter(lst-ctn-kix_list_16-2, decimal) "." counter(lst-ctn-kix_list_16-3, decimal) "." counter(lst-ctn-kix_list_16-4, decimal) "." counter(lst-ctn-kix_list_16-5, decimal) "." counter(lst-ctn-kix_list_16-6, decimal) "." counter(lst-ctn-kix_list_16-7, decimal) "." counter(lst-ctn-kix_list_16-8, decimal) " " + } + + ol.lst-kix_list_20-5.start { + counter-reset: lst-ctn-kix_list_20-5 0 + } + + .lst-kix_list_10-7 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_19-7 { + list-style-type: none + } + + .lst-kix_list_14-6 > li:before { + content: "" counter(lst-ctn-kix_list_14-0, decimal) "." counter(lst-ctn-kix_list_14-1, decimal) "." counter(lst-ctn-kix_list_14-2, decimal) "." counter(lst-ctn-kix_list_14-3, decimal) "." counter(lst-ctn-kix_list_14-4, decimal) "." counter(lst-ctn-kix_list_14-5, decimal) "." counter(lst-ctn-kix_list_14-6, decimal) " " + } + + ol.lst-kix_list_19-6 { + list-style-type: none + } + + ol.lst-kix_list_19-8 { + list-style-type: none + } + + ol.lst-kix_list_19-3 { + list-style-type: none + } + + ol.lst-kix_list_19-2 { + list-style-type: none + } + + .lst-kix_list_10-1 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_19-5 { + list-style-type: none + } + + ol.lst-kix_list_19-4 { + list-style-type: none + } + + ol.lst-kix_list_19-1 { + list-style-type: none + } + + .lst-kix_list_13-1 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_19-0 { + list-style-type: none + } + + .lst-kix_list_23-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_18-8 > li { + counter-increment: lst-ctn-kix_list_18-8 + } + + ol.lst-kix_list_20-4.start { + counter-reset: lst-ctn-kix_list_20-4 0 + } + + ol.lst-kix_list_18-5.start { + counter-reset: lst-ctn-kix_list_18-5 0 + } + + ol.lst-kix_list_6-3.start { + counter-reset: lst-ctn-kix_list_6-3 0 + } + + .lst-kix_list_8-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_1-5 > li:before { + content: "\0025cf " + } + + .lst-kix_list_25-3 > li:before { + content: "" counter(lst-ctn-kix_list_25-0, decimal) "." counter(lst-ctn-kix_list_25-1, decimal) "." counter(lst-ctn-kix_list_25-2, decimal) "." counter(lst-ctn-kix_list_25-3, decimal) " " + } + + .lst-kix_list_29-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_40-0 > li:before { + content: "\00f031 " + } + + .lst-kix_list_24-1 > li:before { + content: "\0025cf " + } + + .lst-kix_list_2-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_20-8 > li { + counter-increment: lst-ctn-kix_list_20-8 + } + + .lst-kix_list_13-8 > li:before { + content: "\0025cf " + } + + .lst-kix_list_16-0 > li { + counter-increment: lst-ctn-kix_list_16-0 + } + + .lst-kix_list_42-6 > li:before { + content: "" counter(lst-ctn-kix_list_42-6, decimal) ". " + } + + ul.lst-kix_list_34-8 { + list-style-type: none + } + + .lst-kix_list_42-2 > li:before { + content: "" counter(lst-ctn-kix_list_42-2, lower-roman) ". " + } + + ul.lst-kix_list_34-7 { + list-style-type: none + } + + ul.lst-kix_list_34-3 { + list-style-type: none + } + + ul.lst-kix_list_34-4 { + list-style-type: none + } + + ul.lst-kix_list_34-5 { + list-style-type: none + } + + .lst-kix_list_39-6 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_34-6 { + list-style-type: none + } + + .lst-kix_list_36-8 > li:before { + content: "\0025aa " + } + + .lst-kix_list_47-6 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_34-0 { + list-style-type: none + } + + ul.lst-kix_list_34-1 { + list-style-type: none + } + + ul.lst-kix_list_34-2 { + list-style-type: none + } + + .lst-kix_list_20-2 > li:before { + content: "" counter(lst-ctn-kix_list_20-0, decimal) "." counter(lst-ctn-kix_list_20-1, decimal) "." counter(lst-ctn-kix_list_20-2, decimal) " " + } + + .lst-kix_list_10-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_21-5 > li:before { + content: "" counter(lst-ctn-kix_list_21-0, decimal) "." counter(lst-ctn-kix_list_21-1, decimal) "." counter(lst-ctn-kix_list_21-2, decimal) "." counter(lst-ctn-kix_list_21-3, decimal) "." counter(lst-ctn-kix_list_21-4, decimal) "." counter(lst-ctn-kix_list_21-5, decimal) " " + } + + ol.lst-kix_list_15-3.start { + counter-reset: lst-ctn-kix_list_15-3 0 + } + + ul.lst-kix_list_40-8 { + list-style-type: none + } + + ul.lst-kix_list_40-7 { + list-style-type: none + } + + ul.lst-kix_list_40-6 { + list-style-type: none + } + + .lst-kix_list_49-0 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_40-5 { + list-style-type: none + } + + ul.lst-kix_list_40-4 { + list-style-type: none + } + + ul.lst-kix_list_40-3 { + list-style-type: none + } + + ol.lst-kix_list_11-0.start { + counter-reset: lst-ctn-kix_list_11-0 0 + } + + .lst-kix_list_15-5 > li { + counter-increment: lst-ctn-kix_list_15-5 + } + + ul.lst-kix_list_40-2 { + list-style-type: none + } + + .lst-kix_list_10-2 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_40-1 { + list-style-type: none + } + + .lst-kix_list_20-7 > li:before { + content: "" counter(lst-ctn-kix_list_20-0, decimal) "." counter(lst-ctn-kix_list_20-1, decimal) "." counter(lst-ctn-kix_list_20-2, decimal) "." counter(lst-ctn-kix_list_20-3, decimal) "." counter(lst-ctn-kix_list_20-4, decimal) "." counter(lst-ctn-kix_list_20-5, decimal) "." counter(lst-ctn-kix_list_20-6, decimal) "." counter(lst-ctn-kix_list_20-7, decimal) " " + } + + ul.lst-kix_list_40-0 { + list-style-type: none + } + + .lst-kix_list_21-4 > li { + counter-increment: lst-ctn-kix_list_21-4 + } + + .lst-kix_list_6-1 > li:before { + content: "" counter(lst-ctn-kix_list_6-0, decimal) "." counter(lst-ctn-kix_list_6-1, decimal) " " + } + + .lst-kix_list_36-1 > li:before { + content: "o " + } + + .lst-kix_list_41-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_17-0 > li:before { + content: "" counter(lst-ctn-kix_list_17-0, decimal) " " + } + + .lst-kix_list_38-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_13-3 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_44-8 { + list-style-type: none + } + + .lst-kix_list_35-1 > li:before { + content: "o " + } + + ul.lst-kix_list_44-4 { + list-style-type: none + } + + .lst-kix_list_14-7 > li:before { + content: "" counter(lst-ctn-kix_list_14-0, decimal) "." counter(lst-ctn-kix_list_14-1, decimal) "." counter(lst-ctn-kix_list_14-2, decimal) "." counter(lst-ctn-kix_list_14-3, decimal) "." counter(lst-ctn-kix_list_14-4, decimal) "." counter(lst-ctn-kix_list_14-5, decimal) "." counter(lst-ctn-kix_list_14-6, decimal) "." counter(lst-ctn-kix_list_14-7, decimal) " " + } + + ul.lst-kix_list_44-5 { + list-style-type: none + } + + ul.lst-kix_list_44-6 { + list-style-type: none + } + + ul.lst-kix_list_44-7 { + list-style-type: none + } + + ul.lst-kix_list_44-0 { + list-style-type: none + } + + ul.lst-kix_list_44-1 { + list-style-type: none + } + + ul.lst-kix_list_44-2 { + list-style-type: none + } + + ul.lst-kix_list_44-3 { + list-style-type: none + } + + .lst-kix_list_40-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_23-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_33-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_12-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_39-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_32-0 > li:before { + content: "" counter(lst-ctn-kix_list_32-0, decimal) ". " + } + + .lst-kix_list_3-7 > li:before { + content: "\0025cf " + } + + .lst-kix_list_49-8 > li:before { + content: "\0025aa " + } + + .lst-kix_list_44-7 > li:before { + content: "o " + } + + ul.lst-kix_list_13-2 { + list-style-type: none + } + + ul.lst-kix_list_13-3 { + list-style-type: none + } + + ul.lst-kix_list_13-4 { + list-style-type: none + } + + ul.lst-kix_list_13-5 { + list-style-type: none + } + + ul.lst-kix_list_13-0 { + list-style-type: none + } + + ul.lst-kix_list_13-1 { + list-style-type: none + } + + .lst-kix_list_12-8 > li:before { + content: "\0025cf " + } + + .lst-kix_list_39-8 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_13-7 { + list-style-type: none + } + + ul.lst-kix_list_13-6 { + list-style-type: none + } + + .lst-kix_list_36-2 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_13-8 { + list-style-type: none + } + + .lst-kix_list_9-8 > li:before { + content: "\0025cf " + } + + .lst-kix_list_16-1 > li:before { + content: "" counter(lst-ctn-kix_list_16-0, decimal) "." counter(lst-ctn-kix_list_16-1, decimal) " " + } + + .lst-kix_list_42-7 > li { + counter-increment: lst-ctn-kix_list_42-7 + } + + .lst-kix_list_8-0 > li:before { + content: "" counter(lst-ctn-kix_list_8-0, lower-latin) ". " + } + + ul.lst-kix_list_47-6 { + list-style-type: none + } + + .lst-kix_list_24-5 > li:before { + content: "\0025cf " + } + + .lst-kix_list_38-1 > li:before { + content: "o " + } + + ul.lst-kix_list_47-5 { + list-style-type: none + } + + ul.lst-kix_list_47-8 { + list-style-type: none + } + + .lst-kix_list_20-1 > li:before { + content: "" counter(lst-ctn-kix_list_20-0, decimal) "." counter(lst-ctn-kix_list_20-1, decimal) " " + } + + ul.lst-kix_list_47-7 { + list-style-type: none + } + + ul.lst-kix_list_49-0 { + list-style-type: none + } + + ol.lst-kix_list_15-6.start { + counter-reset: lst-ctn-kix_list_15-6 0 + } + + ul.lst-kix_list_49-2 { + list-style-type: none + } + + ul.lst-kix_list_49-1 { + list-style-type: none + } + + ul.lst-kix_list_49-3 { + list-style-type: none + } + + ul.lst-kix_list_49-4 { + list-style-type: none + } + + ul.lst-kix_list_49-5 { + list-style-type: none + } + + ul.lst-kix_list_47-0 { + list-style-type: none + } + + ul.lst-kix_list_49-6 { + list-style-type: none + } + + ul.lst-kix_list_47-1 { + list-style-type: none + } + + .lst-kix_list_40-8 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_49-7 { + list-style-type: none + } + + ul.lst-kix_list_47-2 { + list-style-type: none + } + + ul.lst-kix_list_49-8 { + list-style-type: none + } + + ul.lst-kix_list_47-3 { + list-style-type: none + } + + ul.lst-kix_list_47-4 { + list-style-type: none + } + + .lst-kix_list_29-2 > li:before { + content: "\0025aa " + } + + .lst-kix_list_29-8 > li:before { + content: "\0025aa " + } + + .lst-kix_list_35-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_10-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_23-5 > li:before { + content: "\0025cf " + } + + .lst-kix_list_16-1 > li { + counter-increment: lst-ctn-kix_list_16-1 + } + + .lst-kix_list_7-4 > li:before { + content: "\0025cf " + } + + .lst-kix_list_34-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_18-7 > li { + counter-increment: lst-ctn-kix_list_18-7 + } + + ol.lst-kix_list_16-8.start { + counter-reset: lst-ctn-kix_list_16-8 0 + } + + ol.lst-kix_list_20-3.start { + counter-reset: lst-ctn-kix_list_20-3 0 + } + + ol.lst-kix_list_25-6.start { + counter-reset: lst-ctn-kix_list_25-6 0 + } + + .lst-kix_list_36-3 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_19-4.start { + counter-reset: lst-ctn-kix_list_19-4 0 + } + + .lst-kix_list_24-2 > li:before { + content: "\0025cf " + } + + .lst-kix_list_40-7 > li:before { + content: "o " + } + + .lst-kix_list_8-2 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_21-6.start { + counter-reset: lst-ctn-kix_list_21-6 0 + } + + .lst-kix_list_14-3 > li:before { + content: "" counter(lst-ctn-kix_list_14-0, decimal) "." counter(lst-ctn-kix_list_14-1, decimal) "." counter(lst-ctn-kix_list_14-2, decimal) "." counter(lst-ctn-kix_list_14-3, decimal) " " + } + + .lst-kix_list_8-8 > li:before { + content: "\0025cf " + } + + .lst-kix_list_30-2 > li:before { + content: "\0025aa " + } + + ol.lst-kix_list_16-1.start { + counter-reset: lst-ctn-kix_list_16-1 1 + } + + ol.lst-kix_list_20-6.start { + counter-reset: lst-ctn-kix_list_20-6 0 + } + + ul.lst-kix_list_36-7 { + list-style-type: none + } + + ul.lst-kix_list_36-8 { + list-style-type: none + } + + ul.lst-kix_list_36-5 { + list-style-type: none + } + + ul.lst-kix_list_36-6 { + list-style-type: none + } + + ol.lst-kix_list_19-3.start { + counter-reset: lst-ctn-kix_list_19-3 0 + } + + .lst-kix_list_48-3 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_6-2.start { + counter-reset: lst-ctn-kix_list_6-2 0 + } + + ol.lst-kix_list_17-1.start { + counter-reset: lst-ctn-kix_list_17-1 1 + } + + ol.lst-kix_list_42-3.start { + counter-reset: lst-ctn-kix_list_42-3 0 + } + + ul.lst-kix_list_9-0 { + list-style-type: none + } + + .lst-kix_list_22-4 > li:before { + content: "\0025cf " + } + + .lst-kix_list_38-8 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_9-8 { + list-style-type: none + } + + ul.lst-kix_list_36-0 { + list-style-type: none + } + + ul.lst-kix_list_9-7 { + list-style-type: none + } + + .lst-kix_list_19-6 > li:before { + content: "" counter(lst-ctn-kix_list_19-0, decimal) "." counter(lst-ctn-kix_list_19-1, decimal) "." counter(lst-ctn-kix_list_19-2, decimal) "." counter(lst-ctn-kix_list_19-3, decimal) "." counter(lst-ctn-kix_list_19-4, decimal) "." counter(lst-ctn-kix_list_19-5, decimal) "." counter(lst-ctn-kix_list_19-6, decimal) " " + } + + ul.lst-kix_list_9-6 { + list-style-type: none + } + + .lst-kix_list_43-7 > li:before { + content: "o " + } + + ul.lst-kix_list_9-5 { + list-style-type: none + } + + .lst-kix_list_13-6 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_9-4 { + list-style-type: none + } + + ul.lst-kix_list_36-4 { + list-style-type: none + } + + ul.lst-kix_list_9-3 { + list-style-type: none + } + + ul.lst-kix_list_36-3 { + list-style-type: none + } + + .lst-kix_list_46-4 > li:before { + content: "o " + } + + ul.lst-kix_list_9-2 { + list-style-type: none + } + + ul.lst-kix_list_36-2 { + list-style-type: none + } + + ul.lst-kix_list_9-1 { + list-style-type: none + } + + ul.lst-kix_list_36-1 { + list-style-type: none + } + + ul.lst-kix_list_5-1 { + list-style-type: none + } + + ul.lst-kix_list_5-2 { + list-style-type: none + } + + ul.lst-kix_list_5-3 { + list-style-type: none + } + + ul.lst-kix_list_5-4 { + list-style-type: none + } + + .lst-kix_list_10-0 > li:before { + content: "" counter(lst-ctn-kix_list_10-0, lower-latin) ". " + } + + .lst-kix_list_17-7 > li:before { + content: "" counter(lst-ctn-kix_list_17-0, decimal) "." counter(lst-ctn-kix_list_17-1, decimal) "." counter(lst-ctn-kix_list_17-2, decimal) "." counter(lst-ctn-kix_list_17-3, decimal) "." counter(lst-ctn-kix_list_17-4, decimal) "." counter(lst-ctn-kix_list_17-5, decimal) "." counter(lst-ctn-kix_list_17-6, decimal) "." counter(lst-ctn-kix_list_17-7, decimal) " " + } + + .lst-kix_list_40-5 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_5-0 { + list-style-type: none + } + + .lst-kix_list_37-1 > li:before { + content: "o " + } + + .lst-kix_list_13-2 > li:before { + content: "\0025cf " + } + + .lst-kix_list_31-0 > li:before { + content: "\0025a1 " + } + + .lst-kix_list_42-6 > li { + counter-increment: lst-ctn-kix_list_42-6 + } + + ul.lst-kix_list_5-5 { + list-style-type: none + } + + ul.lst-kix_list_5-6 { + list-style-type: none + } + + ul.lst-kix_list_5-7 { + list-style-type: none + } + + ul.lst-kix_list_5-8 { + list-style-type: none + } + + ul.lst-kix_list_7-8 { + list-style-type: none + } + + ul.lst-kix_list_7-7 { + list-style-type: none + } + + .lst-kix_list_4-3 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_7-4 { + list-style-type: none + } + + ul.lst-kix_list_7-3 { + list-style-type: none + } + + ul.lst-kix_list_7-6 { + list-style-type: none + } + + ul.lst-kix_list_7-5 { + list-style-type: none + } + + ul.lst-kix_list_7-2 { + list-style-type: none + } + + ul.lst-kix_list_7-1 { + list-style-type: none + } + + .lst-kix_list_19-4 > li:before { + content: "" counter(lst-ctn-kix_list_19-0, decimal) "." counter(lst-ctn-kix_list_19-1, decimal) "." counter(lst-ctn-kix_list_19-2, decimal) "." counter(lst-ctn-kix_list_19-3, decimal) "." counter(lst-ctn-kix_list_19-4, decimal) " " + } + + .lst-kix_list_14-1 > li:before { + content: "" counter(lst-ctn-kix_list_14-0, decimal) "." counter(lst-ctn-kix_list_14-1, decimal) " " + } + + .lst-kix_list_30-4 > li:before { + content: "o " + } + + ol.lst-kix_list_15-7.start { + counter-reset: lst-ctn-kix_list_15-7 0 + } + + .lst-kix_list_6-8 > li:before { + content: "" counter(lst-ctn-kix_list_6-0, decimal) "." counter(lst-ctn-kix_list_6-1, decimal) "." counter(lst-ctn-kix_list_6-2, decimal) "." counter(lst-ctn-kix_list_6-3, decimal) "." counter(lst-ctn-kix_list_6-4, decimal) "." counter(lst-ctn-kix_list_6-5, decimal) "." counter(lst-ctn-kix_list_6-6, decimal) "." counter(lst-ctn-kix_list_6-7, decimal) "." counter(lst-ctn-kix_list_6-8, decimal) " " + } + + .lst-kix_list_37-2 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_3-7 { + list-style-type: none + } + + ul.lst-kix_list_3-8 { + list-style-type: none + } + + .lst-kix_list_1-6 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_3-0 { + list-style-type: none + } + + ul.lst-kix_list_3-1 { + list-style-type: none + } + + ul.lst-kix_list_3-2 { + list-style-type: none + } + + ul.lst-kix_list_3-3 { + list-style-type: none + } + + .lst-kix_list_16-6 > li { + counter-increment: lst-ctn-kix_list_16-6 + } + + ul.lst-kix_list_3-4 { + list-style-type: none + } + + ul.lst-kix_list_3-5 { + list-style-type: none + } + + ul.lst-kix_list_1-0 { + list-style-type: none + } + + ul.lst-kix_list_3-6 { + list-style-type: none + } + + .lst-kix_list_16-7 > li:before { + content: "" counter(lst-ctn-kix_list_16-0, decimal) "." counter(lst-ctn-kix_list_16-1, decimal) "." counter(lst-ctn-kix_list_16-2, decimal) "." counter(lst-ctn-kix_list_16-3, decimal) "." counter(lst-ctn-kix_list_16-4, decimal) "." counter(lst-ctn-kix_list_16-5, decimal) "." counter(lst-ctn-kix_list_16-6, decimal) "." counter(lst-ctn-kix_list_16-7, decimal) " " + } + + .lst-kix_list_41-5 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_1-2 { + list-style-type: none + } + + ul.lst-kix_list_1-1 { + list-style-type: none + } + + .lst-kix_list_29-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_35-4 > li:before { + content: "o " + } + + ul.lst-kix_list_1-4 { + list-style-type: none + } + + .lst-kix_list_6-8 > li { + counter-increment: lst-ctn-kix_list_6-8 + } + + ul.lst-kix_list_1-3 { + list-style-type: none + } + + ul.lst-kix_list_1-6 { + list-style-type: none + } + + ul.lst-kix_list_1-5 { + list-style-type: none + } + + ul.lst-kix_list_1-8 { + list-style-type: none + } + + ul.lst-kix_list_1-7 { + list-style-type: none + } + + ol.lst-kix_list_19-5.start { + counter-reset: lst-ctn-kix_list_19-5 0 + } + + .lst-kix_list_2-1 > li:before { + content: "\0025cf " + } + + .lst-kix_list_4-8 > li:before { + content: "\0025cf " + } + + .lst-kix_list_36-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_30-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_15-6 > li { + counter-increment: lst-ctn-kix_list_15-6 + } + + .lst-kix_list_31-1 > li:before { + content: "o " + } + + .lst-kix_list_48-1 > li:before { + content: "o " + } + + .lst-kix_list_43-2 > li:before { + content: "\0025aa " + } + + .lst-kix_list_25-3 > li { + counter-increment: lst-ctn-kix_list_25-3 + } + + .lst-kix_list_47-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_38-7 > li:before { + content: "o " + } + + .lst-kix_list_42-4 > li { + counter-increment: lst-ctn-kix_list_42-4 + } + + ul.lst-kix_list_11-3 { + list-style-type: none + } + + .lst-kix_list_47-0 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_11-2 { + list-style-type: none + } + + ol.lst-kix_list_42-6.start { + counter-reset: lst-ctn-kix_list_42-6 0 + } + + ul.lst-kix_list_11-1 { + list-style-type: none + } + + ol.lst-kix_list_20-8.start { + counter-reset: lst-ctn-kix_list_20-8 0 + } + + ul.lst-kix_list_11-7 { + list-style-type: none + } + + ul.lst-kix_list_11-6 { + list-style-type: none + } + + ul.lst-kix_list_11-5 { + list-style-type: none + } + + .lst-kix_list_22-7 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_11-4 { + list-style-type: none + } + + .lst-kix_list_46-8 > li:before { + content: "\0025aa " + } + + .lst-kix_list_14-1 > li { + counter-increment: lst-ctn-kix_list_14-1 + } + + .lst-kix_list_45-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_4-1 > li:before { + content: "\0025cf " + } + + .lst-kix_list_21-0 > li:before { + content: "" counter(lst-ctn-kix_list_21-0, decimal) " " + } + + .lst-kix_list_35-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_1-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_28-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_21-3 > li { + counter-increment: lst-ctn-kix_list_21-3 + } + + .lst-kix_list_21-6 > li { + counter-increment: lst-ctn-kix_list_21-6 + } + + ul.lst-kix_list_11-8 { + list-style-type: none + } + + ul.lst-kix_list_31-8 { + list-style-type: none + } + + .lst-kix_list_17-0 > li { + counter-increment: lst-ctn-kix_list_17-0 + } + + .lst-kix_list_1-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_8-3 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_31-6 { + list-style-type: none + } + + ul.lst-kix_list_31-7 { + list-style-type: none + } + + ul.lst-kix_list_31-4 { + list-style-type: none + } + + ul.lst-kix_list_31-5 { + list-style-type: none + } + + .lst-kix_list_14-0 > li:before { + content: "" counter(lst-ctn-kix_list_14-0, decimal) " " + } + + ul.lst-kix_list_31-2 { + list-style-type: none + } + + ol.lst-kix_list_20-7.start { + counter-reset: lst-ctn-kix_list_20-7 0 + } + + ul.lst-kix_list_31-3 { + list-style-type: none + } + + ul.lst-kix_list_31-0 { + list-style-type: none + } + + .lst-kix_list_19-1 > li { + counter-increment: lst-ctn-kix_list_19-1 + } + + .lst-kix_list_12-0 > li:before { + content: "\002751 " + } + + .lst-kix_list_29-7 > li:before { + content: "o " + } + + ul.lst-kix_list_31-1 { + list-style-type: none + } + + ol.lst-kix_list_15-2.start { + counter-reset: lst-ctn-kix_list_15-2 0 + } + + .lst-kix_list_44-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_15-7 > li:before { + content: "" counter(lst-ctn-kix_list_15-0, decimal) "." counter(lst-ctn-kix_list_15-1, decimal) "." counter(lst-ctn-kix_list_15-2, decimal) "." counter(lst-ctn-kix_list_15-3, decimal) "." counter(lst-ctn-kix_list_15-4, decimal) "." counter(lst-ctn-kix_list_15-5, decimal) "." counter(lst-ctn-kix_list_15-6, decimal) "." counter(lst-ctn-kix_list_15-7, decimal) " " + } + + .lst-kix_list_21-7 > li:before { + content: "" counter(lst-ctn-kix_list_21-0, decimal) "." counter(lst-ctn-kix_list_21-1, decimal) "." counter(lst-ctn-kix_list_21-2, decimal) "." counter(lst-ctn-kix_list_21-3, decimal) "." counter(lst-ctn-kix_list_21-4, decimal) "." counter(lst-ctn-kix_list_21-5, decimal) "." counter(lst-ctn-kix_list_21-6, decimal) "." counter(lst-ctn-kix_list_21-7, decimal) " " + } + + ol.lst-kix_list_15-0.start { + counter-reset: lst-ctn-kix_list_15-0 3 + } + + ol.lst-kix_list_21-5.start { + counter-reset: lst-ctn-kix_list_21-5 0 + } + + .lst-kix_list_42-0 > li:before { + content: "(" counter(lst-ctn-kix_list_42-0, lower-roman) ") " + } + + .lst-kix_list_21-8 > li { + counter-increment: lst-ctn-kix_list_21-8 + } + + .lst-kix_list_26-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_19-5 > li:before { + content: "" counter(lst-ctn-kix_list_19-0, decimal) "." counter(lst-ctn-kix_list_19-1, decimal) "." counter(lst-ctn-kix_list_19-2, decimal) "." counter(lst-ctn-kix_list_19-3, decimal) "." counter(lst-ctn-kix_list_19-4, decimal) "." counter(lst-ctn-kix_list_19-5, decimal) " " + } + + ol.lst-kix_list_42-1.start { + counter-reset: lst-ctn-kix_list_42-1 0 + } + + .lst-kix_list_32-0 > li { + counter-increment: lst-ctn-kix_list_32-0 + } + + .lst-kix_list_35-2 > li:before { + content: "\0025aa " + } + + .lst-kix_list_7-8 > li:before { + content: "\0025cf " + } + + .lst-kix_list_25-1 > li:before { + content: "" counter(lst-ctn-kix_list_25-0, decimal) "." counter(lst-ctn-kix_list_25-1, decimal) " " + } + + .lst-kix_list_24-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_15-1 > li { + counter-increment: lst-ctn-kix_list_15-1 + } + + .lst-kix_list_13-7 > li:before { + content: "\0025cf " + } + + .lst-kix_list_28-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_33-8 > li:before { + content: "\0025aa " + } + + .lst-kix_list_5-1 > li:before { + content: "\0025cf " + } + + .lst-kix_list_44-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_1-1 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_14-0.start { + counter-reset: lst-ctn-kix_list_14-0 0 + } + + .lst-kix_list_32-4 > li:before { + content: "\0025cf " + } + + .lst-kix_list_25-5 > li:before { + content: "" counter(lst-ctn-kix_list_25-0, decimal) "." counter(lst-ctn-kix_list_25-1, decimal) "." counter(lst-ctn-kix_list_25-2, decimal) "." counter(lst-ctn-kix_list_25-3, decimal) "." counter(lst-ctn-kix_list_25-4, decimal) "." counter(lst-ctn-kix_list_25-5, decimal) " " + } + + .lst-kix_list_15-1 > li:before { + content: "" counter(lst-ctn-kix_list_15-0, decimal) "." counter(lst-ctn-kix_list_15-1, decimal) " " + } + + ol.lst-kix_list_18-8.start { + counter-reset: lst-ctn-kix_list_18-8 0 + } + + .lst-kix_list_23-7 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_17-2.start { + counter-reset: lst-ctn-kix_list_17-2 0 + } + + .lst-kix_list_20-8 > li:before { + content: "" counter(lst-ctn-kix_list_20-0, decimal) "." counter(lst-ctn-kix_list_20-1, decimal) "." counter(lst-ctn-kix_list_20-2, decimal) "." counter(lst-ctn-kix_list_20-3, decimal) "." counter(lst-ctn-kix_list_20-4, decimal) "." counter(lst-ctn-kix_list_20-5, decimal) "." counter(lst-ctn-kix_list_20-6, decimal) "." counter(lst-ctn-kix_list_20-7, decimal) "." counter(lst-ctn-kix_list_20-8, decimal) " " + } + + .lst-kix_list_11-7 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_38-2 { + list-style-type: none + } + + .lst-kix_list_16-5 > li { + counter-increment: lst-ctn-kix_list_16-5 + } + + ul.lst-kix_list_38-1 { + list-style-type: none + } + + ul.lst-kix_list_38-0 { + list-style-type: none + } + + .lst-kix_list_5-7 > li:before { + content: "\0025cf " + } + + .lst-kix_list_25-6 > li:before { + content: "" counter(lst-ctn-kix_list_25-0, decimal) "." counter(lst-ctn-kix_list_25-1, decimal) "." counter(lst-ctn-kix_list_25-2, decimal) "." counter(lst-ctn-kix_list_25-3, decimal) "." counter(lst-ctn-kix_list_25-4, decimal) "." counter(lst-ctn-kix_list_25-5, decimal) "." counter(lst-ctn-kix_list_25-6, decimal) " " + } + + .lst-kix_list_40-4 > li:before { + content: "o " + } + + .lst-kix_list_9-4 > li:before { + content: "\0025cf " + } + + .lst-kix_list_34-8 > li:before { + content: "\0025aa " + } + + ol.lst-kix_list_15-8.start { + counter-reset: lst-ctn-kix_list_15-8 0 + } + + .lst-kix_list_45-4 > li:before { + content: "o " + } + + .lst-kix_list_18-1 > li:before { + content: "" counter(lst-ctn-kix_list_18-0, decimal) "." counter(lst-ctn-kix_list_18-1, decimal) " " + } + + ul.lst-kix_list_38-5 { + list-style-type: none + } + + ol.lst-kix_list_42-2.start { + counter-reset: lst-ctn-kix_list_42-2 0 + } + + ul.lst-kix_list_38-6 { + list-style-type: none + } + + ul.lst-kix_list_38-3 { + list-style-type: none + } + + .lst-kix_list_47-2 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_38-4 { + list-style-type: none + } + + ul.lst-kix_list_38-7 { + list-style-type: none + } + + ul.lst-kix_list_38-8 { + list-style-type: none + } + + ul.lst-kix_list_27-1 { + list-style-type: none + } + + ul.lst-kix_list_27-2 { + list-style-type: none + } + + .lst-kix_list_26-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_19-2 > li:before { + content: "" counter(lst-ctn-kix_list_19-0, decimal) "." counter(lst-ctn-kix_list_19-1, decimal) "." counter(lst-ctn-kix_list_19-2, decimal) " " + } + + ul.lst-kix_list_27-0 { + list-style-type: none + } + + ol.lst-kix_list_25-7.start { + counter-reset: lst-ctn-kix_list_25-7 0 + } + + .lst-kix_list_19-8 > li:before { + content: "" counter(lst-ctn-kix_list_19-0, decimal) "." counter(lst-ctn-kix_list_19-1, decimal) "." counter(lst-ctn-kix_list_19-2, decimal) "." counter(lst-ctn-kix_list_19-3, decimal) "." counter(lst-ctn-kix_list_19-4, decimal) "." counter(lst-ctn-kix_list_19-5, decimal) "." counter(lst-ctn-kix_list_19-6, decimal) "." counter(lst-ctn-kix_list_19-7, decimal) "." counter(lst-ctn-kix_list_19-8, decimal) " " + } + + .lst-kix_list_16-2 > li:before { + content: "" counter(lst-ctn-kix_list_16-0, decimal) "." counter(lst-ctn-kix_list_16-1, decimal) "." counter(lst-ctn-kix_list_16-2, decimal) " " + } + + ol.lst-kix_list_16-5.start { + counter-reset: lst-ctn-kix_list_16-5 0 + } + + ol.lst-kix_list_25-5.start { + counter-reset: lst-ctn-kix_list_25-5 0 + } + + .lst-kix_list_18-2 > li:before { + content: "" counter(lst-ctn-kix_list_18-0, decimal) "." counter(lst-ctn-kix_list_18-1, decimal) "." counter(lst-ctn-kix_list_18-2, decimal) " " + } + + .lst-kix_list_42-2 > li { + counter-increment: lst-ctn-kix_list_42-2 + } + + .lst-kix_list_18-1 > li { + counter-increment: lst-ctn-kix_list_18-1 + } + + .lst-kix_list_9-5 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_27-8 { + list-style-type: none + } + + ul.lst-kix_list_27-7 { + list-style-type: none + } + + ul.lst-kix_list_27-6 { + list-style-type: none + } + + ul.lst-kix_list_27-5 { + list-style-type: none + } + + ul.lst-kix_list_27-4 { + list-style-type: none + } + + ol.lst-kix_list_25-1.start { + counter-reset: lst-ctn-kix_list_25-1 0 + } + + ul.lst-kix_list_27-3 { + list-style-type: none + } + + ol.lst-kix_list_19-7.start { + counter-reset: lst-ctn-kix_list_19-7 0 + } + + .lst-kix_list_2-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_6-5 > li:before { + content: "" counter(lst-ctn-kix_list_6-0, decimal) "." counter(lst-ctn-kix_list_6-1, decimal) "." counter(lst-ctn-kix_list_6-2, decimal) "." counter(lst-ctn-kix_list_6-3, decimal) "." counter(lst-ctn-kix_list_6-4, decimal) "." counter(lst-ctn-kix_list_6-5, decimal) " " + } + + .lst-kix_list_17-4 > li { + counter-increment: lst-ctn-kix_list_17-4 + } + + ul.lst-kix_list_23-7 { + list-style-type: none + } + + .lst-kix_list_28-5 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_23-8 { + list-style-type: none + } + + ol.lst-kix_list_25-0.start { + counter-reset: lst-ctn-kix_list_25-0 1 + } + + .lst-kix_list_34-7 > li:before { + content: "o " + } + + ul.lst-kix_list_23-4 { + list-style-type: none + } + + .lst-kix_list_20-7 > li { + counter-increment: lst-ctn-kix_list_20-7 + } + + ul.lst-kix_list_23-3 { + list-style-type: none + } + + ul.lst-kix_list_23-6 { + list-style-type: none + } + + ol.lst-kix_list_14-3.start { + counter-reset: lst-ctn-kix_list_14-3 0 + } + + ul.lst-kix_list_23-5 { + list-style-type: none + } + + ol.lst-kix_list_14-6.start { + counter-reset: lst-ctn-kix_list_14-6 0 + } + + ul.lst-kix_list_23-0 { + list-style-type: none + } + + ol.lst-kix_list_17-6.start { + counter-reset: lst-ctn-kix_list_17-6 0 + } + + ul.lst-kix_list_23-2 { + list-style-type: none + } + + ul.lst-kix_list_23-1 { + list-style-type: none + } + + .lst-kix_list_49-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_20-6 > li:before { + content: "" counter(lst-ctn-kix_list_20-0, decimal) "." counter(lst-ctn-kix_list_20-1, decimal) "." counter(lst-ctn-kix_list_20-2, decimal) "." counter(lst-ctn-kix_list_20-3, decimal) "." counter(lst-ctn-kix_list_20-4, decimal) "." counter(lst-ctn-kix_list_20-5, decimal) "." counter(lst-ctn-kix_list_20-6, decimal) " " + } + + ol.lst-kix_list_10-0 { + list-style-type: none + } + + .lst-kix_list_9-0 > li:before { + content: "\002751 " + } + + .lst-kix_list_49-4 > li:before { + content: "o " + } + + .lst-kix_list_14-5 > li { + counter-increment: lst-ctn-kix_list_14-5 + } + + .lst-kix_list_7-0 > li:before { + content: "" counter(lst-ctn-kix_list_7-0, lower-latin) ". " + } + + .lst-kix_list_19-6 > li { + counter-increment: lst-ctn-kix_list_19-6 + } + + .lst-kix_list_3-5 > li:before { + content: "\0025cf " + } + + .lst-kix_list_44-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_29-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_17-1 > li { + counter-increment: lst-ctn-kix_list_17-1 + } + + ol.lst-kix_list_7-0.start { + counter-reset: lst-ctn-kix_list_7-0 0 + } + + .lst-kix_list_19-3 > li:before { + content: "" counter(lst-ctn-kix_list_19-0, decimal) "." counter(lst-ctn-kix_list_19-1, decimal) "." counter(lst-ctn-kix_list_19-2, decimal) "." counter(lst-ctn-kix_list_19-3, decimal) " " + } + + .lst-kix_list_19-3 > li { + counter-increment: lst-ctn-kix_list_19-3 + } + + .lst-kix_list_10-0 > li { + counter-increment: lst-ctn-kix_list_10-0 + } + + .lst-kix_list_4-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_17-8 > li:before { + content: "" counter(lst-ctn-kix_list_17-0, decimal) "." counter(lst-ctn-kix_list_17-1, decimal) "." counter(lst-ctn-kix_list_17-2, decimal) "." counter(lst-ctn-kix_list_17-3, decimal) "." counter(lst-ctn-kix_list_17-4, decimal) "." counter(lst-ctn-kix_list_17-5, decimal) "." counter(lst-ctn-kix_list_17-6, decimal) "." counter(lst-ctn-kix_list_17-7, decimal) "." counter(lst-ctn-kix_list_17-8, decimal) " " + } + + ol.lst-kix_list_6-4 { + list-style-type: none + } + + ol.lst-kix_list_6-5 { + list-style-type: none + } + + .lst-kix_list_16-3 > li { + counter-increment: lst-ctn-kix_list_16-3 + } + + ol.lst-kix_list_6-2 { + list-style-type: none + } + + .lst-kix_list_20-2 > li { + counter-increment: lst-ctn-kix_list_20-2 + } + + ol.lst-kix_list_6-3 { + list-style-type: none + } + + ol.lst-kix_list_20-1 { + list-style-type: none + } + + ol.lst-kix_list_6-8 { + list-style-type: none + } + + ol.lst-kix_list_20-0 { + list-style-type: none + } + + ol.lst-kix_list_20-3 { + list-style-type: none + } + + ol.lst-kix_list_6-6 { + list-style-type: none + } + + ol.lst-kix_list_20-2 { + list-style-type: none + } + + .lst-kix_list_12-5 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_6-7 { + list-style-type: none + } + + ol.lst-kix_list_20-4 { + list-style-type: none + } + + ol.lst-kix_list_20-5 { + list-style-type: none + } + + .lst-kix_list_3-0 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_20-6 { + list-style-type: none + } + + ol.lst-kix_list_20-7 { + list-style-type: none + } + + ol.lst-kix_list_20-8 { + list-style-type: none + } + + .lst-kix_list_12-2 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_6-1 { + list-style-type: none + } + + .lst-kix_list_19-7 > li:before { + content: "" counter(lst-ctn-kix_list_19-0, decimal) "." counter(lst-ctn-kix_list_19-1, decimal) "." counter(lst-ctn-kix_list_19-2, decimal) "." counter(lst-ctn-kix_list_19-3, decimal) "." counter(lst-ctn-kix_list_19-4, decimal) "." counter(lst-ctn-kix_list_19-5, decimal) "." counter(lst-ctn-kix_list_19-6, decimal) "." counter(lst-ctn-kix_list_19-7, decimal) " " + } + + ol.lst-kix_list_6-0 { + list-style-type: none + } + + .lst-kix_list_5-4 > li:before { + content: "\0025cf " + } + + .lst-kix_list_32-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_33-7 > li:before { + content: "o " + } + + ol.lst-kix_list_6-6.start { + counter-reset: lst-ctn-kix_list_6-6 0 + } + + ol.lst-kix_list_42-0.start { + counter-reset: lst-ctn-kix_list_42-0 0 + } + + ol.lst-kix_list_42-5.start { + counter-reset: lst-ctn-kix_list_42-5 0 + } + + .lst-kix_list_6-5 > li { + counter-increment: lst-ctn-kix_list_6-5 + } + + ol.lst-kix_list_14-2.start { + counter-reset: lst-ctn-kix_list_14-2 0 + } + + ol.lst-kix_list_18-7.start { + counter-reset: lst-ctn-kix_list_18-7 0 + } + + ol.lst-kix_list_17-2 { + list-style-type: none + } + + ol.lst-kix_list_17-3 { + list-style-type: none + } + + ol.lst-kix_list_17-0 { + list-style-type: none + } + + ol.lst-kix_list_17-1 { + list-style-type: none + } + + ol.lst-kix_list_17-6 { + list-style-type: none + } + + ol.lst-kix_list_17-7 { + list-style-type: none + } + + ol.lst-kix_list_17-4 { + list-style-type: none + } + + ol.lst-kix_list_17-5 { + list-style-type: none + } + + .lst-kix_list_25-1 > li { + counter-increment: lst-ctn-kix_list_25-1 + } + + .lst-kix_list_10-8 > li:before { + content: "\0025cf " + } + + .lst-kix_list_35-3 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_17-8 { + list-style-type: none + } + + .lst-kix_list_6-2 > li { + counter-increment: lst-ctn-kix_list_6-2 + } + + .lst-kix_list_13-5 > li:before { + content: "\0025cf " + } + + .lst-kix_list_13-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_34-1 > li:before { + content: "o " + } + + .lst-kix_list_44-4 > li:before { + content: "o " + } + + ol.lst-kix_list_21-3.start { + counter-reset: lst-ctn-kix_list_21-3 0 + } + + .lst-kix_list_19-2 > li { + counter-increment: lst-ctn-kix_list_19-2 + } + + .lst-kix_list_6-7 > li { + counter-increment: lst-ctn-kix_list_6-7 + } + + .lst-kix_list_45-7 > li:before { + content: "o " + } + + ol.lst-kix_list_14-4.start { + counter-reset: lst-ctn-kix_list_14-4 0 + } + + .lst-kix_list_43-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_42-5 > li { + counter-increment: lst-ctn-kix_list_42-5 + } + + .lst-kix_list_32-5 > li:before { + content: "\0025cf " + } + + .lst-kix_list_32-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_15-5 > li:before { + content: "" counter(lst-ctn-kix_list_15-0, decimal) "." counter(lst-ctn-kix_list_15-1, decimal) "." counter(lst-ctn-kix_list_15-2, decimal) "." counter(lst-ctn-kix_list_15-3, decimal) "." counter(lst-ctn-kix_list_15-4, decimal) "." counter(lst-ctn-kix_list_15-5, decimal) " " + } + + .lst-kix_list_6-3 > li:before { + content: "" counter(lst-ctn-kix_list_6-0, decimal) "." counter(lst-ctn-kix_list_6-1, decimal) "." counter(lst-ctn-kix_list_6-2, decimal) "." counter(lst-ctn-kix_list_6-3, decimal) " " + } + + .lst-kix_list_44-8 > li:before { + content: "\0025aa " + } + + .lst-kix_list_11-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_37-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_20-4 > li { + counter-increment: lst-ctn-kix_list_20-4 + } + + ol.lst-kix_list_17-5.start { + counter-reset: lst-ctn-kix_list_17-5 0 + } + + .lst-kix_list_31-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_24-8 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_15-4.start { + counter-reset: lst-ctn-kix_list_15-4 0 + } + + .lst-kix_list_27-2 > li:before { + content: "\0025aa " + } + + .lst-kix_list_45-1 > li:before { + content: "o " + } + + .lst-kix_list_37-7 > li:before { + content: "o " + } + + .lst-kix_list_3-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_41-6 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_16-3.start { + counter-reset: lst-ctn-kix_list_16-3 0 + } + + .lst-kix_list_22-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_26-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_38-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_34-5 > li:before { + content: "\0025aa " + } + + ol.lst-kix_list_42-8 { + list-style-type: none + } + + ol.lst-kix_list_42-7 { + list-style-type: none + } + + ol.lst-kix_list_42-6 { + list-style-type: none + } + + ol.lst-kix_list_42-5 { + list-style-type: none + } + + ol.lst-kix_list_42-4 { + list-style-type: none + } + + ol.lst-kix_list_42-2 { + list-style-type: none + } + + ol.lst-kix_list_42-3 { + list-style-type: none + } + + ol.lst-kix_list_42-0 { + list-style-type: none + } + + ol.lst-kix_list_42-1 { + list-style-type: none + } + + .lst-kix_list_5-5 > li:before { + content: "\0025cf " + } + + .lst-kix_list_19-7 > li { + counter-increment: lst-ctn-kix_list_19-7 + } + + .lst-kix_list_18-5 > li { + counter-increment: lst-ctn-kix_list_18-5 + } + + ol.lst-kix_list_17-0.start { + counter-reset: lst-ctn-kix_list_17-0 1 + } + + .lst-kix_list_39-4 > li:before { + content: "o " + } + + .lst-kix_list_9-2 > li:before { + content: "\0025cf " + } + + .lst-kix_list_42-3 > li { + counter-increment: lst-ctn-kix_list_42-3 + } + + .lst-kix_list_15-8 > li { + counter-increment: lst-ctn-kix_list_15-8 + } + + ul.lst-kix_list_28-2 { + list-style-type: none + } + + ol.lst-kix_list_16-1 { + list-style-type: none + } + + ul.lst-kix_list_28-3 { + list-style-type: none + } + + ol.lst-kix_list_16-2 { + list-style-type: none + } + + ul.lst-kix_list_28-4 { + list-style-type: none + } + + ol.lst-kix_list_16-3 { + list-style-type: none + } + + ul.lst-kix_list_28-5 { + list-style-type: none + } + + .lst-kix_list_28-1 > li:before { + content: "o " + } + + ol.lst-kix_list_16-4 { + list-style-type: none + } + + .lst-kix_list_44-1 > li:before { + content: "o " + } + + ul.lst-kix_list_28-6 { + list-style-type: none + } + + ul.lst-kix_list_28-7 { + list-style-type: none + } + + ul.lst-kix_list_28-8 { + list-style-type: none + } + + .lst-kix_list_20-4 > li:before { + content: "" counter(lst-ctn-kix_list_20-0, decimal) "." counter(lst-ctn-kix_list_20-1, decimal) "." counter(lst-ctn-kix_list_20-2, decimal) "." counter(lst-ctn-kix_list_20-3, decimal) "." counter(lst-ctn-kix_list_20-4, decimal) " " + } + + ol.lst-kix_list_16-0 { + list-style-type: none + } + + .lst-kix_list_41-4 > li:before { + content: "o " + } + + .lst-kix_list_32-1 > li:before { + content: "\0025cf " + } + + .lst-kix_list_31-8 > li:before { + content: "\0025aa " + } + + ol.lst-kix_list_16-5 { + list-style-type: none + } + + .lst-kix_list_42-5 > li:before { + content: "" counter(lst-ctn-kix_list_42-5, lower-roman) ". " + } + + .lst-kix_list_28-2 > li:before { + content: "\0025aa " + } + + ol.lst-kix_list_16-6 { + list-style-type: none + } + + ol.lst-kix_list_16-7 { + list-style-type: none + } + + ol.lst-kix_list_16-8 { + list-style-type: none + } + + .lst-kix_list_38-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_30-1 > li:before { + content: "o " + } + + .lst-kix_list_5-8 > li:before { + content: "\0025cf " + } + + .lst-kix_list_14-5 > li:before { + content: "" counter(lst-ctn-kix_list_14-0, decimal) "." counter(lst-ctn-kix_list_14-1, decimal) "." counter(lst-ctn-kix_list_14-2, decimal) "." counter(lst-ctn-kix_list_14-3, decimal) "." counter(lst-ctn-kix_list_14-4, decimal) "." counter(lst-ctn-kix_list_14-5, decimal) " " + } + + .lst-kix_list_40-1 > li:before { + content: "o " + } + + ol.lst-kix_list_19-8.start { + counter-reset: lst-ctn-kix_list_19-8 0 + } + + ul.lst-kix_list_28-1 { + list-style-type: none + } + + .lst-kix_list_42-3 > li:before { + content: "" counter(lst-ctn-kix_list_42-3, decimal) ". " + } + + ul.lst-kix_list_28-0 { + list-style-type: none + } + + ul.lst-kix_list_22-6 { + list-style-type: none + } + + ul.lst-kix_list_22-7 { + list-style-type: none + } + + ul.lst-kix_list_22-4 { + list-style-type: none + } + + ul.lst-kix_list_22-5 { + list-style-type: none + } + + ul.lst-kix_list_22-2 { + list-style-type: none + } + + ul.lst-kix_list_22-3 { + list-style-type: none + } + + ul.lst-kix_list_22-0 { + list-style-type: none + } + + .lst-kix_list_42-8 > li { + counter-increment: lst-ctn-kix_list_42-8 + } + + ol.lst-kix_list_42-8.start { + counter-reset: lst-ctn-kix_list_42-8 0 + } + + ul.lst-kix_list_22-1 { + list-style-type: none + } + + .lst-kix_list_43-8 > li:before { + content: "\0025aa " + } + + .lst-kix_list_21-1 > li:before { + content: "" counter(lst-ctn-kix_list_21-0, decimal) "." counter(lst-ctn-kix_list_21-1, decimal) " " + } + + .lst-kix_list_15-3 > li:before { + content: "" counter(lst-ctn-kix_list_15-0, decimal) "." counter(lst-ctn-kix_list_15-1, decimal) "." counter(lst-ctn-kix_list_15-2, decimal) "." counter(lst-ctn-kix_list_15-3, decimal) " " + } + + .lst-kix_list_39-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_11-8 > li:before { + content: "\0025cf " + } + + .lst-kix_list_6-0 > li:before { + content: "" counter(lst-ctn-kix_list_6-0, decimal) " " + } + + .lst-kix_list_40-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_15-2 > li { + counter-increment: lst-ctn-kix_list_15-2 + } + + .lst-kix_list_28-4 > li:before { + content: "o " + } + + ul.lst-kix_list_22-8 { + list-style-type: none + } + + .lst-kix_list_24-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_45-8 > li:before { + content: "\0025aa " + } + + .lst-kix_list_42-0 > li { + counter-increment: lst-ctn-kix_list_42-0 + } + + .lst-kix_list_46-2 > li:before { + content: "\0025aa " + } + + ol.lst-kix_list_20-1.start { + counter-reset: lst-ctn-kix_list_20-1 6 + } + + .lst-kix_list_29-1 > li:before { + content: "o " + } + + .lst-kix_list_46-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_8-7 > li:before { + content: "\0025cf " + } + + .lst-kix_list_1-7 > li:before { + content: "\0025cf " + } + + .lst-kix_list_35-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_28-8 > li:before { + content: "\0025aa " + } + + .lst-kix_list_33-1 > li:before { + content: "o " + } + + .lst-kix_list_15-0 > li { + counter-increment: lst-ctn-kix_list_15-0 + } + + .lst-kix_list_20-1 > li { + counter-increment: lst-ctn-kix_list_20-1 + } + + .lst-kix_list_7-3 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_12-8 { + list-style-type: none + } + + .lst-kix_list_30-5 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_12-7 { + list-style-type: none + } + + .lst-kix_list_1-2 > li:before { + content: "\0025cf " + } + + .lst-kix_list_22-3 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_12-0 { + list-style-type: none + } + + ul.lst-kix_list_12-1 { + list-style-type: none + } + + ol.lst-kix_list_6-0.start { + counter-reset: lst-ctn-kix_list_6-0 1 + } + + ul.lst-kix_list_12-2 { + list-style-type: none + } + + ul.lst-kix_list_12-3 { + list-style-type: none + } + + .lst-kix_list_19-0 > li:before { + content: "" counter(lst-ctn-kix_list_19-0, decimal) " " + } + + ul.lst-kix_list_12-4 { + list-style-type: none + } + + ul.lst-kix_list_12-5 { + list-style-type: none + } + + .lst-kix_list_25-2 > li { + counter-increment: lst-ctn-kix_list_25-2 + } + + ul.lst-kix_list_12-6 { + list-style-type: none + } + + .lst-kix_list_49-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_12-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_38-6 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_21-0.start { + counter-reset: lst-ctn-kix_list_21-0 0 + } + + ul.lst-kix_list_33-8 { + list-style-type: none + } + + .lst-kix_list_43-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_48-7 > li:before { + content: "o " + } + + .lst-kix_list_14-4 > li { + counter-increment: lst-ctn-kix_list_14-4 + } + + .lst-kix_list_27-7 > li:before { + content: "o " + } + + .lst-kix_list_2-7 > li:before { + content: "\0025cf " + } + + .lst-kix_list_17-7 > li { + counter-increment: lst-ctn-kix_list_17-7 + } + + ul.lst-kix_list_33-0 { + list-style-type: none + } + + .lst-kix_list_25-0 > li:before { + content: "" counter(lst-ctn-kix_list_25-0, decimal) " " + } + + ul.lst-kix_list_33-1 { + list-style-type: none + } + + ul.lst-kix_list_33-2 { + list-style-type: none + } + + ul.lst-kix_list_33-3 { + list-style-type: none + } + + ol.lst-kix_list_42-7.start { + counter-reset: lst-ctn-kix_list_42-7 0 + } + + ul.lst-kix_list_33-4 { + list-style-type: none + } + + .lst-kix_list_18-2 > li { + counter-increment: lst-ctn-kix_list_18-2 + } + + ul.lst-kix_list_33-5 { + list-style-type: none + } + + .lst-kix_list_14-8 > li:before { + content: "" counter(lst-ctn-kix_list_14-0, decimal) "." counter(lst-ctn-kix_list_14-1, decimal) "." counter(lst-ctn-kix_list_14-2, decimal) "." counter(lst-ctn-kix_list_14-3, decimal) "." counter(lst-ctn-kix_list_14-4, decimal) "." counter(lst-ctn-kix_list_14-5, decimal) "." counter(lst-ctn-kix_list_14-6, decimal) "." counter(lst-ctn-kix_list_14-7, decimal) "." counter(lst-ctn-kix_list_14-8, decimal) " " + } + + ul.lst-kix_list_33-6 { + list-style-type: none + } + + ul.lst-kix_list_33-7 { + list-style-type: none + } + + .lst-kix_list_27-4 > li:before { + content: "o " + } + + ul.lst-kix_list_41-2 { + list-style-type: none + } + + ul.lst-kix_list_41-1 { + list-style-type: none + } + + ul.lst-kix_list_41-0 { + list-style-type: none + } + + ul.lst-kix_list_41-6 { + list-style-type: none + } + + ul.lst-kix_list_41-5 { + list-style-type: none + } + + ol.lst-kix_list_19-6.start { + counter-reset: lst-ctn-kix_list_19-6 0 + } + + ul.lst-kix_list_41-4 { + list-style-type: none + } + + ul.lst-kix_list_41-3 { + list-style-type: none + } + + .lst-kix_list_18-8 > li:before { + content: "" counter(lst-ctn-kix_list_18-0, decimal) "." counter(lst-ctn-kix_list_18-1, decimal) "." counter(lst-ctn-kix_list_18-2, decimal) "." counter(lst-ctn-kix_list_18-3, decimal) "." counter(lst-ctn-kix_list_18-4, decimal) "." counter(lst-ctn-kix_list_18-5, decimal) "." counter(lst-ctn-kix_list_18-6, decimal) "." counter(lst-ctn-kix_list_18-7, decimal) "." counter(lst-ctn-kix_list_18-8, decimal) " " + } + + .lst-kix_list_21-2 > li:before { + content: "" counter(lst-ctn-kix_list_21-0, decimal) "." counter(lst-ctn-kix_list_21-1, decimal) "." counter(lst-ctn-kix_list_21-2, decimal) " " + } + + ul.lst-kix_list_41-8 { + list-style-type: none + } + + ul.lst-kix_list_41-7 { + list-style-type: none + } + + .lst-kix_list_14-4 > li:before { + content: "" counter(lst-ctn-kix_list_14-0, decimal) "." counter(lst-ctn-kix_list_14-1, decimal) "." counter(lst-ctn-kix_list_14-2, decimal) "." counter(lst-ctn-kix_list_14-3, decimal) "." counter(lst-ctn-kix_list_14-4, decimal) " " + } + + ol.lst-kix_list_21-1.start { + counter-reset: lst-ctn-kix_list_21-1 6 + } + + .lst-kix_list_30-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_26-4 > li:before { + content: "o " + } + + .lst-kix_list_6-1 > li { + counter-increment: lst-ctn-kix_list_6-1 + } + + .lst-kix_list_34-2 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_45-8 { + list-style-type: none + } + + ul.lst-kix_list_43-2 { + list-style-type: none + } + + .lst-kix_list_4-7 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_45-7 { + list-style-type: none + } + + ul.lst-kix_list_43-1 { + list-style-type: none + } + + ul.lst-kix_list_43-4 { + list-style-type: none + } + + ul.lst-kix_list_43-3 { + list-style-type: none + } + + ul.lst-kix_list_43-6 { + list-style-type: none + } + + ul.lst-kix_list_43-5 { + list-style-type: none + } + + ul.lst-kix_list_43-8 { + list-style-type: none + } + + .lst-kix_list_14-6 > li { + counter-increment: lst-ctn-kix_list_14-6 + } + + ul.lst-kix_list_43-7 { + list-style-type: none + } + + .lst-kix_list_18-4 > li { + counter-increment: lst-ctn-kix_list_18-4 + } + + .lst-kix_list_37-8 > li:before { + content: "\0025aa " + } + + .lst-kix_list_16-4 > li:before { + content: "" counter(lst-ctn-kix_list_16-0, decimal) "." counter(lst-ctn-kix_list_16-1, decimal) "." counter(lst-ctn-kix_list_16-2, decimal) "." counter(lst-ctn-kix_list_16-3, decimal) "." counter(lst-ctn-kix_list_16-4, decimal) " " + } + + ul.lst-kix_list_43-0 { + list-style-type: none + } + + .lst-kix_list_43-4 > li:before { + content: "o " + } + + .lst-kix_list_17-3 > li { + counter-increment: lst-ctn-kix_list_17-3 + } + + .lst-kix_list_49-1 > li:before { + content: "o " + } + + ul.lst-kix_list_45-0 { + list-style-type: none + } + + ul.lst-kix_list_45-1 { + list-style-type: none + } + + .lst-kix_list_23-8 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_45-2 { + list-style-type: none + } + + ul.lst-kix_list_45-3 { + list-style-type: none + } + + ul.lst-kix_list_45-4 { + list-style-type: none + } + + ul.lst-kix_list_45-5 { + list-style-type: none + } + + .lst-kix_list_17-1 > li:before { + content: "" counter(lst-ctn-kix_list_17-0, decimal) "." counter(lst-ctn-kix_list_17-1, decimal) " " + } + + ul.lst-kix_list_45-6 { + list-style-type: none + } + + .lst-kix_list_6-6 > li:before { + content: "" counter(lst-ctn-kix_list_6-0, decimal) "." counter(lst-ctn-kix_list_6-1, decimal) "." counter(lst-ctn-kix_list_6-2, decimal) "." counter(lst-ctn-kix_list_6-3, decimal) "." counter(lst-ctn-kix_list_6-4, decimal) "." counter(lst-ctn-kix_list_6-5, decimal) "." counter(lst-ctn-kix_list_6-6, decimal) " " + } + + .lst-kix_list_42-8 > li:before { + content: "" counter(lst-ctn-kix_list_42-8, lower-roman) ". " + } + + .lst-kix_list_21-5 > li { + counter-increment: lst-ctn-kix_list_21-5 + } + + .lst-kix_list_16-6 > li:before { + content: "" counter(lst-ctn-kix_list_16-0, decimal) "." counter(lst-ctn-kix_list_16-1, decimal) "." counter(lst-ctn-kix_list_16-2, decimal) "." counter(lst-ctn-kix_list_16-3, decimal) "." counter(lst-ctn-kix_list_16-4, decimal) "." counter(lst-ctn-kix_list_16-5, decimal) "." counter(lst-ctn-kix_list_16-6, decimal) " " + } + + .lst-kix_list_43-6 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_16-7.start { + counter-reset: lst-ctn-kix_list_16-7 0 + } + + ul.lst-kix_list_48-1 { + list-style-type: none + } + + ol.lst-kix_list_18-4.start { + counter-reset: lst-ctn-kix_list_18-4 0 + } + + ul.lst-kix_list_48-0 { + list-style-type: none + } + + ul.lst-kix_list_48-3 { + list-style-type: none + } + + ul.lst-kix_list_48-2 { + list-style-type: none + } + + ul.lst-kix_list_46-7 { + list-style-type: none + } + + ul.lst-kix_list_46-6 { + list-style-type: none + } + + ol.lst-kix_list_14-8 { + list-style-type: none + } + + ul.lst-kix_list_46-8 { + list-style-type: none + } + + ol.lst-kix_list_14-7 { + list-style-type: none + } + + ol.lst-kix_list_21-4.start { + counter-reset: lst-ctn-kix_list_21-4 0 + } + + ol.lst-kix_list_14-6 { + list-style-type: none + } + + ol.lst-kix_list_14-5 { + list-style-type: none + } + + ol.lst-kix_list_14-4 { + list-style-type: none + } + + .lst-kix_list_5-3 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_14-3 { + list-style-type: none + } + + ol.lst-kix_list_14-2 { + list-style-type: none + } + + ol.lst-kix_list_14-1 { + list-style-type: none + } + + ol.lst-kix_list_14-0 { + list-style-type: none + } + + .lst-kix_list_11-0 > li:before { + content: "" counter(lst-ctn-kix_list_11-0, decimal) ". " + } + + ul.lst-kix_list_46-2 { + list-style-type: none + } + + ul.lst-kix_list_48-8 { + list-style-type: none + } + + ul.lst-kix_list_46-3 { + list-style-type: none + } + + ul.lst-kix_list_46-4 { + list-style-type: none + } + + ul.lst-kix_list_46-5 { + list-style-type: none + } + + ul.lst-kix_list_48-4 { + list-style-type: none + } + + ul.lst-kix_list_48-5 { + list-style-type: none + } + + ul.lst-kix_list_46-0 { + list-style-type: none + } + + ul.lst-kix_list_48-6 { + list-style-type: none + } + + ul.lst-kix_list_46-1 { + list-style-type: none + } + + ul.lst-kix_list_48-7 { + list-style-type: none + } + + .lst-kix_list_19-4 > li { + counter-increment: lst-ctn-kix_list_19-4 + } + + .lst-kix_list_47-4 > li:before { + content: "o " + } + + .lst-kix_list_16-2 > li { + counter-increment: lst-ctn-kix_list_16-2 + } + + .lst-kix_list_20-6 > li { + counter-increment: lst-ctn-kix_list_20-6 + } + + .lst-kix_list_8-0 > li { + counter-increment: lst-ctn-kix_list_8-0 + } + + ul.lst-kix_list_4-8 { + list-style-type: none + } + + ul.lst-kix_list_4-7 { + list-style-type: none + } + + ul.lst-kix_list_4-6 { + list-style-type: none + } + + .lst-kix_list_17-2 > li:before { + content: "" counter(lst-ctn-kix_list_17-0, decimal) "." counter(lst-ctn-kix_list_17-1, decimal) "." counter(lst-ctn-kix_list_17-2, decimal) " " + } + + .lst-kix_list_11-4 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_4-1 { + list-style-type: none + } + + .lst-kix_list_48-8 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_4-0 { + list-style-type: none + } + + .lst-kix_list_7-5 > li:before { + content: "\0025cf " + } + + .lst-kix_list_46-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_10-4 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_4-5 { + list-style-type: none + } + + ul.lst-kix_list_4-4 { + list-style-type: none + } + + .lst-kix_list_25-6 > li { + counter-increment: lst-ctn-kix_list_25-6 + } + + ul.lst-kix_list_4-3 { + list-style-type: none + } + + ul.lst-kix_list_4-2 { + list-style-type: none + } + + .lst-kix_list_27-1 > li:before { + content: "o " + } + + .lst-kix_list_25-7 > li { + counter-increment: lst-ctn-kix_list_25-7 + } + + .lst-kix_list_26-8 > li:before { + content: "\0025aa " + } + + .lst-kix_list_49-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_30-7 > li:before { + content: "o " + } + + ol.lst-kix_list_18-6.start { + counter-reset: lst-ctn-kix_list_18-6 0 + } + + .lst-kix_list_15-6 > li:before { + content: "" counter(lst-ctn-kix_list_15-0, decimal) "." counter(lst-ctn-kix_list_15-1, decimal) "." counter(lst-ctn-kix_list_15-2, decimal) "." counter(lst-ctn-kix_list_15-3, decimal) "." counter(lst-ctn-kix_list_15-4, decimal) "." counter(lst-ctn-kix_list_15-5, decimal) "." counter(lst-ctn-kix_list_15-6, decimal) " " + } + + .lst-kix_list_18-4 > li:before { + content: "" counter(lst-ctn-kix_list_18-0, decimal) "." counter(lst-ctn-kix_list_18-1, decimal) "." counter(lst-ctn-kix_list_18-2, decimal) "." counter(lst-ctn-kix_list_18-3, decimal) "." counter(lst-ctn-kix_list_18-4, decimal) " " + } + + .lst-kix_list_10-5 > li:before { + content: "\0025cf " + } + + .lst-kix_list_48-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_17-2 > li { + counter-increment: lst-ctn-kix_list_17-2 + } + + ul.lst-kix_list_35-8 { + list-style-type: none + } + + ul.lst-kix_list_35-6 { + list-style-type: none + } + + ul.lst-kix_list_35-7 { + list-style-type: none + } + + .lst-kix_list_45-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_6-6 > li { + counter-increment: lst-ctn-kix_list_6-6 + } + + ul.lst-kix_list_35-5 { + list-style-type: none + } + + ul.lst-kix_list_35-4 { + list-style-type: none + } + + ul.lst-kix_list_35-3 { + list-style-type: none + } + + ul.lst-kix_list_35-2 { + list-style-type: none + } + + .lst-kix_list_17-3 > li:before { + content: "" counter(lst-ctn-kix_list_17-0, decimal) "." counter(lst-ctn-kix_list_17-1, decimal) "." counter(lst-ctn-kix_list_17-2, decimal) "." counter(lst-ctn-kix_list_17-3, decimal) " " + } + + ul.lst-kix_list_35-1 { + list-style-type: none + } + + ul.lst-kix_list_35-0 { + list-style-type: none + } + + .lst-kix_list_15-2 > li:before { + content: "" counter(lst-ctn-kix_list_15-0, decimal) "." counter(lst-ctn-kix_list_15-1, decimal) "." counter(lst-ctn-kix_list_15-2, decimal) " " + } + + .lst-kix_list_6-2 > li:before { + content: "" counter(lst-ctn-kix_list_6-0, decimal) "." counter(lst-ctn-kix_list_6-1, decimal) "." counter(lst-ctn-kix_list_6-2, decimal) " " + } + + .lst-kix_list_19-1 > li:before { + content: "" counter(lst-ctn-kix_list_19-0, decimal) "." counter(lst-ctn-kix_list_19-1, decimal) " " + } + + ul.lst-kix_list_39-0 { + list-style-type: none + } + + ul.lst-kix_list_39-1 { + list-style-type: none + } + + .lst-kix_list_6-3 > li { + counter-increment: lst-ctn-kix_list_6-3 + } + + ol.lst-kix_list_10-0.start { + counter-reset: lst-ctn-kix_list_10-0 0 + } + + .lst-kix_list_19-0 > li { + counter-increment: lst-ctn-kix_list_19-0 + } + + ol.lst-kix_list_6-4.start { + counter-reset: lst-ctn-kix_list_6-4 0 + } + + ul.lst-kix_list_39-7 { + list-style-type: none + } + + ol.lst-kix_list_16-0.start { + counter-reset: lst-ctn-kix_list_16-0 1 + } + + ul.lst-kix_list_39-6 { + list-style-type: none + } + + ul.lst-kix_list_39-8 { + list-style-type: none + } + + ol.lst-kix_list_25-4.start { + counter-reset: lst-ctn-kix_list_25-4 0 + } + + ul.lst-kix_list_39-3 { + list-style-type: none + } + + ul.lst-kix_list_39-2 { + list-style-type: none + } + + .lst-kix_list_15-4 > li { + counter-increment: lst-ctn-kix_list_15-4 + } + + ul.lst-kix_list_39-5 { + list-style-type: none + } + + ul.lst-kix_list_39-4 { + list-style-type: none + } + + ol.lst-kix_list_16-2.start { + counter-reset: lst-ctn-kix_list_16-2 0 + } + + ol.lst-kix_list_21-2.start { + counter-reset: lst-ctn-kix_list_21-2 0 + } + + .lst-kix_list_31-3 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_14-1.start { + counter-reset: lst-ctn-kix_list_14-1 12 + } + + ol.lst-kix_list_18-1.start { + counter-reset: lst-ctn-kix_list_18-1 6 + } + + .lst-kix_list_28-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_8-4 > li:before { + content: "\0025cf " + } + + .lst-kix_list_47-1 > li:before { + content: "o " + } + + .lst-kix_list_15-7 > li { + counter-increment: lst-ctn-kix_list_15-7 + } + + .lst-kix_list_26-7 > li:before { + content: "o " + } + + .lst-kix_list_12-1 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_14-8.start { + counter-reset: lst-ctn-kix_list_14-8 0 + } + + .lst-kix_list_31-4 > li:before { + content: "o " + } + + .lst-kix_list_20-3 > li:before { + content: "" counter(lst-ctn-kix_list_20-0, decimal) "." counter(lst-ctn-kix_list_20-1, decimal) "." counter(lst-ctn-kix_list_20-2, decimal) "." counter(lst-ctn-kix_list_20-3, decimal) " " + } + + .lst-kix_list_19-5 > li { + counter-increment: lst-ctn-kix_list_19-5 + } + + .lst-kix_list_49-2 > li:before { + content: "\0025aa " + } + + .lst-kix_list_16-3 > li:before { + content: "" counter(lst-ctn-kix_list_16-0, decimal) "." counter(lst-ctn-kix_list_16-1, decimal) "." counter(lst-ctn-kix_list_16-2, decimal) "." counter(lst-ctn-kix_list_16-3, decimal) " " + } + + .lst-kix_list_25-0 > li { + counter-increment: lst-ctn-kix_list_25-0 + } + + .lst-kix_list_34-4 > li:before { + content: "o " + } + + .lst-kix_list_39-3 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_8-0.start { + counter-reset: lst-ctn-kix_list_8-0 3 + } + + .lst-kix_list_4-2 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_17-8.start { + counter-reset: lst-ctn-kix_list_17-8 0 + } + + .lst-kix_list_24-4 > li:before { + content: "\0025cf " + } + + .lst-kix_list_47-8 > li:before { + content: "\0025aa " + } + + .lst-kix_list_42-1 > li:before { + content: "" counter(lst-ctn-kix_list_42-1, lower-latin) ". " + } + + .lst-kix_list_17-6 > li:before { + content: "" counter(lst-ctn-kix_list_17-0, decimal) "." counter(lst-ctn-kix_list_17-1, decimal) "." counter(lst-ctn-kix_list_17-2, decimal) "." counter(lst-ctn-kix_list_17-3, decimal) "." counter(lst-ctn-kix_list_17-4, decimal) "." counter(lst-ctn-kix_list_17-5, decimal) "." counter(lst-ctn-kix_list_17-6, decimal) " " + } + + .lst-kix_list_21-8 > li:before { + content: "" counter(lst-ctn-kix_list_21-0, decimal) "." counter(lst-ctn-kix_list_21-1, decimal) "." counter(lst-ctn-kix_list_21-2, decimal) "." counter(lst-ctn-kix_list_21-3, decimal) "." counter(lst-ctn-kix_list_21-4, decimal) "." counter(lst-ctn-kix_list_21-5, decimal) "." counter(lst-ctn-kix_list_21-6, decimal) "." counter(lst-ctn-kix_list_21-7, decimal) "." counter(lst-ctn-kix_list_21-8, decimal) " " + } + + ol.lst-kix_list_42-4.start { + counter-reset: lst-ctn-kix_list_42-4 0 + } + + .lst-kix_list_31-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_18-5 > li:before { + content: "" counter(lst-ctn-kix_list_18-0, decimal) "." counter(lst-ctn-kix_list_18-1, decimal) "." counter(lst-ctn-kix_list_18-2, decimal) "." counter(lst-ctn-kix_list_18-3, decimal) "." counter(lst-ctn-kix_list_18-4, decimal) "." counter(lst-ctn-kix_list_18-5, decimal) " " + } + + .lst-kix_list_3-4 > li:before { + content: "\0025cf " + } + + .lst-kix_list_36-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_18-6 > li:before { + content: "" counter(lst-ctn-kix_list_18-0, decimal) "." counter(lst-ctn-kix_list_18-1, decimal) "." counter(lst-ctn-kix_list_18-2, decimal) "." counter(lst-ctn-kix_list_18-3, decimal) "." counter(lst-ctn-kix_list_18-4, decimal) "." counter(lst-ctn-kix_list_18-5, decimal) "." counter(lst-ctn-kix_list_18-6, decimal) " " + } + + .lst-kix_list_27-0 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_26-4 { + list-style-type: none + } + + ul.lst-kix_list_32-3 { + list-style-type: none + } + + ul.lst-kix_list_26-5 { + list-style-type: none + } + + ul.lst-kix_list_32-4 { + list-style-type: none + } + + ul.lst-kix_list_26-6 { + list-style-type: none + } + + ul.lst-kix_list_32-1 { + list-style-type: none + } + + ul.lst-kix_list_24-0 { + list-style-type: none + } + + ul.lst-kix_list_24-1 { + list-style-type: none + } + + ul.lst-kix_list_26-7 { + list-style-type: none + } + + ul.lst-kix_list_32-2 { + list-style-type: none + } + + ul.lst-kix_list_24-2 { + list-style-type: none + } + + ul.lst-kix_list_26-8 { + list-style-type: none + } + + .lst-kix_list_26-1 > li:before { + content: "o " + } + + ul.lst-kix_list_32-7 { + list-style-type: none + } + + ul.lst-kix_list_24-3 { + list-style-type: none + } + + ul.lst-kix_list_32-8 { + list-style-type: none + } + + ul.lst-kix_list_24-4 { + list-style-type: none + } + + .lst-kix_list_49-7 > li:before { + content: "o " + } + + ol.lst-kix_list_32-0 { + list-style-type: none + } + + ul.lst-kix_list_32-5 { + list-style-type: none + } + + ul.lst-kix_list_24-5 { + list-style-type: none + } + + ul.lst-kix_list_32-6 { + list-style-type: none + } + + ul.lst-kix_list_24-7 { + list-style-type: none + } + + ul.lst-kix_list_24-6 { + list-style-type: none + } + + .lst-kix_list_12-4 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_24-8 { + list-style-type: none + } + + ul.lst-kix_list_26-1 { + list-style-type: none + } + + .lst-kix_list_43-1 > li:before { + content: "o " + } + + ul.lst-kix_list_26-0 { + list-style-type: none + } + + ul.lst-kix_list_26-3 { + list-style-type: none + } + + ul.lst-kix_list_26-2 { + list-style-type: none + } + + ol.lst-kix_list_17-7.start { + counter-reset: lst-ctn-kix_list_17-7 0 + } + + .lst-kix_list_25-8 > li { + counter-increment: lst-ctn-kix_list_25-8 + } + + .lst-kix_list_11-1 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_14-5.start { + counter-reset: lst-ctn-kix_list_14-5 0 + } + + .lst-kix_list_33-4 > li:before { + content: "o " + } + + .lst-kix_list_46-1 > li:before { + content: "o " + } + + .lst-kix_list_44-2 > li:before { + content: "\0025aa " + } + + ol.lst-kix_list_19-0.start { + counter-reset: lst-ctn-kix_list_19-0 0 + } + + .lst-kix_list_17-4 > li:before { + content: "" counter(lst-ctn-kix_list_17-0, decimal) "." counter(lst-ctn-kix_list_17-1, decimal) "." counter(lst-ctn-kix_list_17-2, decimal) "." counter(lst-ctn-kix_list_17-3, decimal) "." counter(lst-ctn-kix_list_17-4, decimal) " " + } + + .lst-kix_list_48-4 > li:before { + content: "o " + } + + .lst-kix_list_47-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_14-3 > li { + counter-increment: lst-ctn-kix_list_14-3 + } + + .lst-kix_list_3-1 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_2-4 { + list-style-type: none + } + + ol.lst-kix_list_19-2.start { + counter-reset: lst-ctn-kix_list_19-2 0 + } + + ul.lst-kix_list_2-5 { + list-style-type: none + } + + ul.lst-kix_list_2-6 { + list-style-type: none + } + + ul.lst-kix_list_2-7 { + list-style-type: none + } + + ul.lst-kix_list_2-0 { + list-style-type: none + } + + .lst-kix_list_45-2 > li:before { + content: "\0025aa " + } + + .lst-kix_list_42-7 > li:before { + content: "" counter(lst-ctn-kix_list_42-7, lower-latin) ". " + } + + ul.lst-kix_list_2-1 { + list-style-type: none + } + + .lst-kix_list_23-3 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_2-2 { + list-style-type: none + } + + ul.lst-kix_list_2-3 { + list-style-type: none + } + + ol.lst-kix_list_18-0.start { + counter-reset: lst-ctn-kix_list_18-0 0 + } + + .lst-kix_list_15-3 > li { + counter-increment: lst-ctn-kix_list_15-3 + } + + .lst-kix_list_21-1 > li { + counter-increment: lst-ctn-kix_list_21-1 + } + + ul.lst-kix_list_2-8 { + list-style-type: none + } + + .lst-kix_list_9-7 > li:before { + content: "\0025cf " + } + + .lst-kix_list_23-2 > li:before { + content: "\0025cf " + } + + .lst-kix_list_22-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_15-0 > li:before { + content: "" counter(lst-ctn-kix_list_15-0, decimal) " " + } + + .lst-kix_list_11-0 > li { + counter-increment: lst-ctn-kix_list_11-0 + } + + .lst-kix_list_3-2 > li:before { + content: "\0025cf " + } + + .lst-kix_list_24-0 > li:before { + content: "\0025cf " + } + + .lst-kix_list_7-0 > li { + counter-increment: lst-ctn-kix_list_7-0 + } + + .lst-kix_list_32-8 > li:before { + content: "\0025cf " + } + + .lst-kix_list_29-4 > li:before { + content: "o " + } + + .lst-kix_list_36-7 > li:before { + content: "o " + } + + .lst-kix_list_39-7 > li:before { + content: "o " + } + + .lst-kix_list_17-5 > li:before { + content: "" counter(lst-ctn-kix_list_17-0, decimal) "." counter(lst-ctn-kix_list_17-1, decimal) "." counter(lst-ctn-kix_list_17-2, decimal) "." counter(lst-ctn-kix_list_17-3, decimal) "." counter(lst-ctn-kix_list_17-4, decimal) "." counter(lst-ctn-kix_list_17-5, decimal) " " + } + + ul.lst-kix_list_10-7 { + list-style-type: none + } + + ul.lst-kix_list_10-8 { + list-style-type: none + } + + ul.lst-kix_list_10-5 { + list-style-type: none + } + + .lst-kix_list_39-1 > li:before { + content: "o " + } + + .lst-kix_list_11-5 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_10-6 { + list-style-type: none + } + + .lst-kix_list_11-2 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_10-3 { + list-style-type: none + } + + ul.lst-kix_list_10-4 { + list-style-type: none + } + + .lst-kix_list_22-5 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_10-1 { + list-style-type: none + } + + .lst-kix_list_1-4 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_10-2 { + list-style-type: none + } + + .lst-kix_list_22-2 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_25-4 { + list-style-type: none + } + + ol.lst-kix_list_25-3 { + list-style-type: none + } + + .lst-kix_list_48-0 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_25-6 { + list-style-type: none + } + + ol.lst-kix_list_25-5 { + list-style-type: none + } + + ol.lst-kix_list_25-0 { + list-style-type: none + } + + .lst-kix_list_18-3 > li { + counter-increment: lst-ctn-kix_list_18-3 + } + + ol.lst-kix_list_25-2 { + list-style-type: none + } + + ol.lst-kix_list_25-1 { + list-style-type: none + } + + .lst-kix_list_16-4 > li { + counter-increment: lst-ctn-kix_list_16-4 + } + + ul.lst-kix_list_37-3 { + list-style-type: none + } + + .lst-kix_list_44-5 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_37-2 { + list-style-type: none + } + + ul.lst-kix_list_37-1 { + list-style-type: none + } + + ul.lst-kix_list_37-0 { + list-style-type: none + } + + ol.lst-kix_list_17-4.start { + counter-reset: lst-ctn-kix_list_17-4 0 + } + + .lst-kix_list_20-3 > li { + counter-increment: lst-ctn-kix_list_20-3 + } + + ol.lst-kix_list_25-8 { + list-style-type: none + } + + ol.lst-kix_list_25-7 { + list-style-type: none + } + + .lst-kix_list_14-8 > li { + counter-increment: lst-ctn-kix_list_14-8 + } + + .lst-kix_list_46-0 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_6-7.start { + counter-reset: lst-ctn-kix_list_6-7 0 + } + + ul.lst-kix_list_37-8 { + list-style-type: none + } + + .lst-kix_list_20-5 > li:before { + content: "" counter(lst-ctn-kix_list_20-0, decimal) "." counter(lst-ctn-kix_list_20-1, decimal) "." counter(lst-ctn-kix_list_20-2, decimal) "." counter(lst-ctn-kix_list_20-3, decimal) "." counter(lst-ctn-kix_list_20-4, decimal) "." counter(lst-ctn-kix_list_20-5, decimal) " " + } + + ul.lst-kix_list_37-6 { + list-style-type: none + } + + ul.lst-kix_list_37-7 { + list-style-type: none + } + + .lst-kix_list_1-8 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_37-4 { + list-style-type: none + } + + ul.lst-kix_list_37-5 { + list-style-type: none + } + + ol.lst-kix_list_15-5.start { + counter-reset: lst-ctn-kix_list_15-5 0 + } + + .lst-kix_list_3-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_20-5 > li { + counter-increment: lst-ctn-kix_list_20-5 + } + + .lst-kix_list_25-8 > li:before { + content: "" counter(lst-ctn-kix_list_25-0, decimal) "." counter(lst-ctn-kix_list_25-1, decimal) "." counter(lst-ctn-kix_list_25-2, decimal) "." counter(lst-ctn-kix_list_25-3, decimal) "." counter(lst-ctn-kix_list_25-4, decimal) "." counter(lst-ctn-kix_list_25-5, decimal) "." counter(lst-ctn-kix_list_25-6, decimal) "." counter(lst-ctn-kix_list_25-7, decimal) "." counter(lst-ctn-kix_list_25-8, decimal) " " + } + + .lst-kix_list_25-5 > li { + counter-increment: lst-ctn-kix_list_25-5 + } + + ol.lst-kix_list_20-0.start { + counter-reset: lst-ctn-kix_list_20-0 0 + } + + ol.lst-kix_list_6-1.start { + counter-reset: lst-ctn-kix_list_6-1 3 + } + + .lst-kix_list_6-4 > li:before { + content: "" counter(lst-ctn-kix_list_6-0, decimal) "." counter(lst-ctn-kix_list_6-1, decimal) "." counter(lst-ctn-kix_list_6-2, decimal) "." counter(lst-ctn-kix_list_6-3, decimal) "." counter(lst-ctn-kix_list_6-4, decimal) " " + } + + .lst-kix_list_18-7 > li:before { + content: "" counter(lst-ctn-kix_list_18-0, decimal) "." counter(lst-ctn-kix_list_18-1, decimal) "." counter(lst-ctn-kix_list_18-2, decimal) "." counter(lst-ctn-kix_list_18-3, decimal) "." counter(lst-ctn-kix_list_18-4, decimal) "." counter(lst-ctn-kix_list_18-5, decimal) "." counter(lst-ctn-kix_list_18-6, decimal) "." counter(lst-ctn-kix_list_18-7, decimal) " " + } + + .lst-kix_list_14-2 > li { + counter-increment: lst-ctn-kix_list_14-2 + } + + ol.lst-kix_list_16-6.start { + counter-reset: lst-ctn-kix_list_16-6 0 + } + + .lst-kix_list_6-4 > li { + counter-increment: lst-ctn-kix_list_6-4 + } + + ol.lst-kix_list_6-8.start { + counter-reset: lst-ctn-kix_list_6-8 0 + } + + .lst-kix_list_18-6 > li { + counter-increment: lst-ctn-kix_list_18-6 + } + + .lst-kix_list_16-0 > li:before { + content: "" counter(lst-ctn-kix_list_16-0, decimal) " " + } + + ol.lst-kix_list_21-7.start { + counter-reset: lst-ctn-kix_list_21-7 0 + } + + .lst-kix_list_23-4 > li:before { + content: "\0025cf " + } + + .lst-kix_list_16-5 > li:before { + content: "" counter(lst-ctn-kix_list_16-0, decimal) "." counter(lst-ctn-kix_list_16-1, decimal) "." counter(lst-ctn-kix_list_16-2, decimal) "." counter(lst-ctn-kix_list_16-3, decimal) "." counter(lst-ctn-kix_list_16-4, decimal) "." counter(lst-ctn-kix_list_16-5, decimal) " " + } + + ol.lst-kix_list_18-2.start { + counter-reset: lst-ctn-kix_list_18-2 0 + } + + ol.lst-kix_list_21-8.start { + counter-reset: lst-ctn-kix_list_21-8 0 + } + + .lst-kix_list_14-0 > li { + counter-increment: lst-ctn-kix_list_14-0 + } + + .lst-kix_list_2-2 > li:before { + content: "\0025cf " + } + + .lst-kix_list_47-7 > li:before { + content: "o " + } + + ol.lst-kix_list_25-8.start { + counter-reset: lst-ctn-kix_list_25-8 0 + } + + .lst-kix_list_7-7 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_14-7.start { + counter-reset: lst-ctn-kix_list_14-7 0 + } + + .lst-kix_list_7-1 > li:before { + content: "\0025cf " + } + + .lst-kix_list_32-2 > li:before { + content: "\0025cf " + } + + .lst-kix_list_33-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_2-8 > li:before { + content: "\0025cf " + } + + .lst-kix_list_27-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_29-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_21-6 > li:before { + content: "" counter(lst-ctn-kix_list_21-0, decimal) "." counter(lst-ctn-kix_list_21-1, decimal) "." counter(lst-ctn-kix_list_21-2, decimal) "." counter(lst-ctn-kix_list_21-3, decimal) "." counter(lst-ctn-kix_list_21-4, decimal) "." counter(lst-ctn-kix_list_21-5, decimal) "." counter(lst-ctn-kix_list_21-6, decimal) " " + } + + .lst-kix_list_41-1 > li:before { + content: "o " + } + + .lst-kix_list_27-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_20-0 > li:before { + content: "" counter(lst-ctn-kix_list_20-0, decimal) " " + } + + ol.lst-kix_list_20-2.start { + counter-reset: lst-ctn-kix_list_20-2 0 + } + + .lst-kix_list_27-8 > li:before { + content: "\0025aa " + } + + .lst-kix_list_11-6 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_8-4 { + list-style-type: none + } + + ul.lst-kix_list_8-5 { + list-style-type: none + } + + ul.lst-kix_list_8-2 { + list-style-type: none + } + + ul.lst-kix_list_8-3 { + list-style-type: none + } + + ul.lst-kix_list_8-8 { + list-style-type: none + } + + ul.lst-kix_list_8-6 { + list-style-type: none + } + + .lst-kix_list_42-4 > li:before { + content: "" counter(lst-ctn-kix_list_42-4, lower-latin) ". " + } + + ul.lst-kix_list_8-7 { + list-style-type: none + } + + .lst-kix_list_4-5 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_15-1.start { + counter-reset: lst-ctn-kix_list_15-1 2 + } + + ul.lst-kix_list_8-1 { + list-style-type: none + } + + .lst-kix_list_26-2 > li:before { + content: "\0025aa " + } + + .lst-kix_list_37-3 > li:before { + content: "\0025cf " + } + + .lst-kix_list_2-5 > li:before { + content: "\0025cf " + } + + .lst-kix_list_7-6 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_7-0 { + list-style-type: none + } + + ol.lst-kix_list_32-0.start { + counter-reset: lst-ctn-kix_list_32-0 0 + } + + .lst-kix_list_4-4 > li:before { + content: "\0025cf " + } + + .lst-kix_list_30-0 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_30-4 { + list-style-type: none + } + + ul.lst-kix_list_30-3 { + list-style-type: none + } + + ul.lst-kix_list_30-6 { + list-style-type: none + } + + ul.lst-kix_list_30-5 { + list-style-type: none + } + + ul.lst-kix_list_30-8 { + list-style-type: none + } + + .lst-kix_list_35-8 > li:before { + content: "\0025aa " + } + + ol.lst-kix_list_25-3.start { + counter-reset: lst-ctn-kix_list_25-3 0 + } + + ul.lst-kix_list_30-7 { + list-style-type: none + } + + .lst-kix_list_36-6 > li:before { + content: "\0025cf " + } + + ul.lst-kix_list_30-0 { + list-style-type: none + } + + ul.lst-kix_list_30-2 { + list-style-type: none + } + + .lst-kix_list_38-2 > li:before { + content: "\0025aa " + } + + ul.lst-kix_list_30-1 { + list-style-type: none + } + + .lst-kix_list_5-0 > li:before { + content: "\0025cf " + } + + ol.lst-kix_list_18-7 { + list-style-type: none + } + + ol.lst-kix_list_18-8 { + list-style-type: none + } + + ol.lst-kix_list_18-5 { + list-style-type: none + } + + .lst-kix_list_25-4 > li:before { + content: "" counter(lst-ctn-kix_list_25-0, decimal) "." counter(lst-ctn-kix_list_25-1, decimal) "." counter(lst-ctn-kix_list_25-2, decimal) "." counter(lst-ctn-kix_list_25-3, decimal) "." counter(lst-ctn-kix_list_25-4, decimal) " " + } + + .lst-kix_list_14-2 > li:before { + content: "" counter(lst-ctn-kix_list_14-0, decimal) "." counter(lst-ctn-kix_list_14-1, decimal) "." counter(lst-ctn-kix_list_14-2, decimal) " " + } + + ol.lst-kix_list_18-6 { + list-style-type: none + } + + ol.lst-kix_list_18-3 { + list-style-type: none + } + + ol.lst-kix_list_18-4 { + list-style-type: none + } + + ol.lst-kix_list_18-1 { + list-style-type: none + } + + .lst-kix_list_17-5 > li { + counter-increment: lst-ctn-kix_list_17-5 + } + + .lst-kix_list_15-4 > li:before { + content: "" counter(lst-ctn-kix_list_15-0, decimal) "." counter(lst-ctn-kix_list_15-1, decimal) "." counter(lst-ctn-kix_list_15-2, decimal) "." counter(lst-ctn-kix_list_15-3, decimal) "." counter(lst-ctn-kix_list_15-4, decimal) " " + } + + ol.lst-kix_list_18-2 { + list-style-type: none + } + + ol.lst-kix_list_18-0 { + list-style-type: none + } + + .lst-kix_list_3-8 > li:before { + content: "\0025cf " + } + + .lst-kix_list_26-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_48-2 > li:before { + content: "\0025aa " + } + + .lst-kix_list_21-7 > li { + counter-increment: lst-ctn-kix_list_21-7 + } + + .lst-kix_list_33-5 > li:before { + content: "\0025aa " + } + + .lst-kix_list_5-6 > li:before { + content: "\0025cf " + } + + .lst-kix_list_16-8 > li { + counter-increment: lst-ctn-kix_list_16-8 + } + + .lst-kix_list_23-1 > li:before { + content: "\0025cf " + } + + .lst-kix_list_25-4 > li { + counter-increment: lst-ctn-kix_list_25-4 + } + + .lst-kix_list_20-0 > li { + counter-increment: lst-ctn-kix_list_20-0 + } + + .lst-kix_list_24-7 > li:before { + content: "\0025cf " + } + + ol { + margin: 0; + padding: 0 + } + + .c1 { + border-bottom-width: 1pt; + border-top-style: solid; + width: 193.2pt; + border-right-style: solid; + padding: 0pt 5.8pt 0pt 5.8pt; + border-bottom-color: #000000; + border-top-width: 1pt; + border-bottom-style: solid; + vertical-align: top; + border-top-color: #000000; + border-left-color: #000000; + border-right-color: #000000; + border-left-style: solid; + border-right-width: 1pt; + border-left-width: 1pt + } + + .c20 { + border-bottom-width: 1pt; + border-top-style: solid; + width: 238.2pt; + border-right-style: solid; + padding: 0pt 5.8pt 0pt 5.8pt; + border-bottom-color: #000000; + border-top-width: 1pt; + border-bottom-style: solid; + vertical-align: top; + border-top-color: #000000; + border-left-color: #000000; + border-right-color: #000000; + border-left-style: solid; + border-right-width: 1pt; + border-left-width: 1pt + } + + .c3 { + padding-left: 0pt; + line-height: 1.0; + padding-top: 0pt; + margin-left: 36pt; + padding-bottom: 0pt + } + + .c12 { + vertical-align: baseline; + color: #000000; + font-style: normal; + text-decoration: none + } + + .c4 { + line-height: 1.0; + padding-top: 0pt; + text-align: left; + padding-bottom: 0pt + } + + .c11 { + max-width: 432pt; + background-color: #ffffff; + padding: 72pt 90pt 72pt 90pt + } + + .c2 { + widows: 2; + orphans: 2; + direction: ltr + } + + .c16 { + margin-right: auto; + border-collapse: collapse; + margin-left: -5.8pt + } + + .c9 { + line-height: 1.0; + padding-top: 36pt; + padding-bottom: 0pt + } + + .c21 { + line-height: 1.0; + padding-top: 0pt; + padding-bottom: 36pt + } + + .c15 { + font-size: 14pt; + font-family: "Calibri" + } + + .c18 { + font-size: 12pt; + font-family: "Times New Roman" + } + + .c6 { + margin: 0; + padding: 0 + } + + .c0 { + font-size: 12pt; + font-family: "Calibri" + } + + .c22 { + font-size: 10pt; + font-family: "Times New Roman" + } + + .c13 { + color: #000000; + background-color: #ffff00 + } + + .c14 { + font-size: 9pt; + font-family: "Times New Roman" + } + + .c10 { + text-decoration: underline + } + + .c17 { + color: #ff0000 + } + + .c7 { + font-weight: bold + } + + .c19 { + height: 0pt + } + + .c8 { + font-weight: normal + } + + .c5 { + height: 10pt + } + + .title { + widows: 2; + padding-top: 0pt; + line-height: 1.0; + orphans: 2; + text-align: center; + color: #000000; + font-size: 14pt; + font-family: "Times New Roman"; + font-weight: bold; + padding-bottom: 0pt; + page-break-after: avoid + } + + .subtitle { + widows: 2; + padding-top: 18pt; + line-height: 1.0; + orphans: 2; + text-align: left; + color: #666666; + font-style: italic; + font-size: 24pt; + font-family: "Georgia"; + padding-bottom: 4pt; + page-break-after: avoid + } + + li { + color: #000000; + font-size: 10pt; + font-family: "Times New Roman" + } + + p { + color: #000000; + font-size: 10pt; + margin: 0; + font-family: "Times New Roman" + } + + h1 { + widows: 2; + padding-top: 0pt; + line-height: 1.0; + orphans: 2; + text-align: center; + color: #000000; + font-size: 12pt; + font-family: "Times New Roman"; + font-weight: bold; + padding-bottom: 0pt; + page-break-after: avoid + } + + h2 { + widows: 2; + padding-top: 0pt; + line-height: 1.0; + orphans: 2; + text-align: left; + color: #000000; + font-size: 10pt; + font-family: "Times New Roman"; + font-weight: bold; + padding-bottom: 0pt; + page-break-after: avoid + } + + h3 { + widows: 2; + padding-top: 0pt; + line-height: 1.0; + orphans: 2; + text-align: left; + color: #000000; + font-size: 10pt; + text-decoration: underline; + font-family: "Times New Roman"; + padding-bottom: 0pt; + page-break-after: avoid + } + + h4 { + widows: 2; + padding-top: 12pt; + line-height: 1.0; + orphans: 2; + text-align: left; + color: #000000; + font-size: 12pt; + font-family: "Times New Roman"; + font-weight: bold; + padding-bottom: 2pt; + page-break-after: avoid + } + + h5 { + widows: 2; + padding-top: 11pt; + line-height: 1.0; + orphans: 2; + text-align: left; + color: #000000; + font-size: 11pt; + font-family: "Times New Roman"; + font-weight: bold; + padding-bottom: 2pt; + page-break-after: avoid + } + + h6 { + widows: 2; + padding-top: 10pt; + line-height: 1.0; + orphans: 2; + text-align: left; + color: #000000; + font-size: 10pt; + font-family: "Times New Roman"; + font-weight: bold; + padding-bottom: 2pt; + page-break-after: avoid + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/minimal/minimal.css.scss b/web/app/assets/stylesheets/minimal/minimal.css.scss index 9ec8c234a..f97d05c7f 100644 --- a/web/app/assets/stylesheets/minimal/minimal.css.scss +++ b/web/app/assets/stylesheets/minimal/minimal.css.scss @@ -1,4 +1,5 @@ /** +*= require web/Raleway *= require client/ie *= require client/jamkazam *= require client/screen_common diff --git a/web/app/assets/stylesheets/web/Raleway.css b/web/app/assets/stylesheets/web/Raleway.css new file mode 100644 index 000000000..14c53019b --- /dev/null +++ b/web/app/assets/stylesheets/web/Raleway.css @@ -0,0 +1,36 @@ +@font-face { + font-family: 'Raleway'; + font-style: normal; + font-weight: 100; + src: local('Raleway Thin'), local('Raleway-Thin'), url(/assets/Raleway/Raleway-Thin.ttf) format('truetype'), url(/assets/Raleway/Raleway-Thin.woff2) format('woff2'), url(/assets/Raleway/Raleway-Thin.woff) format('woff'); +} + +@font-face { + font-family: 'Raleway'; + font-style: normal; + font-weight: 300; + src: local('Raleway Light'), local('Raleway-Light'), url(/assets/Raleway/Raleway-Light.ttf) format('truetype'), url(/assets/Raleway/Raleway-Light.woff2) format('woff2'), url(/assets/Raleway/Raleway-Light.woff) format('woff'); +} + +@font-face { + font-family: 'Raleway'; + font-style: normal; + font-weight: 400; + src: url(/assets/Raleway/Raleway.eot); + src: local('Raleway'), url(/assets/Raleway/Raleway.eot?#iefix) format('embedded-opentype'), url(/assets/Raleway/Raleway.svg#Raleway) format('svg'), url(/assets/Raleway/Raleway.ttf) format('truetype'), url(/assets/Raleway/Raleway.woff2) format('woff2'), url(/assets/Raleway/Raleway.woff) format('woff'); +} + +@font-face { + font-family: 'Raleway'; + font-style: normal; + font-weight: 700; + src: local('Raleway Bold'), local('Raleway-Bold'), url(/assets/Raleway/Raleway-Bold.ttf) format('truetype'), url(/assets/Raleway/Raleway-Bold.woff2) format('woff2'), url(/assets/Raleway/Raleway-Bold.woff) format('woff'); +} + +@font-face { + font-family: 'Raleway'; + font-style: normal; + font-weight: 900; + src: local('Raleway Heavy'), local('Raleway-Heavy'), url(/assets/Raleway/Raleway-Heavy.ttf) format('truetype'), url(/assets/Raleway/Raleway-Heavy.woff2) format('woff2'), url(/assets/Raleway/Raleway-Heavy.woff) format('woff'); +} + diff --git a/web/app/assets/stylesheets/web/affiliate_links.css.scss b/web/app/assets/stylesheets/web/affiliate_links.css.scss new file mode 100644 index 000000000..b788146b5 --- /dev/null +++ b/web/app/assets/stylesheets/web/affiliate_links.css.scss @@ -0,0 +1,65 @@ +@import 'client/common.css.scss'; + +body.affiliate_links { + + h2 { + color:white !important; + } + + h2, h3 { + margin-bottom:2px; + } + + .intro { + margin-bottom:30px; + } + + .link-type-prompt { + color:$ColorTextTypical; + line-height:125%; + + + p { + margin-bottom:20px; + } + + .example-link { + font-weight:bold; + } + + } + + table.links-table { + min-width:100%; + margin-top:20px; + margin-bottom:30px; + + th { + padding: 10px 0 20px; + white-space:nowrap; + } + + td { + padding:3px 0; + white-space:nowrap; + } + .target { + width:45%; + white-space:normal; + } + .copy-link { + width:100px; + } + + .url { + + input { + background-color: transparent; + -webkit-box-shadow:none; + box-shadow:none; + color:#ccc; + width:100%; + } + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/web/web.css b/web/app/assets/stylesheets/web/web.css index 5761892c5..12c614f5c 100644 --- a/web/app/assets/stylesheets/web/web.css +++ b/web/app/assets/stylesheets/web/web.css @@ -1,4 +1,6 @@ /** + +*= require ./Raleway *= require client/jamServer *= require client/ie *= require client/jamkazam @@ -28,6 +30,8 @@ *= require users/signinCommon *= require dialogs/dialog *= require client/help +*= require landings/partner_agreement_v1 +*= require web/affiliate_links *= require_directory ../landings *= require icheck/minimal/minimal */ \ No newline at end of file diff --git a/web/app/controllers/affiliates_controller.rb b/web/app/controllers/affiliates_controller.rb new file mode 100644 index 000000000..2b5ed0876 --- /dev/null +++ b/web/app/controllers/affiliates_controller.rb @@ -0,0 +1,12 @@ +class AffiliatesController < ApplicationController + + respond_to :html + + def links + + @partner = AffiliatePartner.find(params[:id]) + + render 'affiliate_links', layout: 'web' + end +end + diff --git a/web/app/controllers/api_affiliate_controller.rb b/web/app/controllers/api_affiliate_controller.rb new file mode 100644 index 000000000..98b59e89d --- /dev/null +++ b/web/app/controllers/api_affiliate_controller.rb @@ -0,0 +1,53 @@ +class ApiAffiliateController < ApiController + + before_filter :api_signed_in_user, :except => [ :create ] + + include ErrorsHelper + + respond_to :json + + def log + @log || Logging.logger[ApiAffiliateController] + end + + def create + if current_user + # is this user already an affiliate? Then kick this back + if current_user.affiliate_partner + render :json => simple_error('affiliate_partner', 'You are already an affiliate.'), status: 422 + return + else + @partner = AffiliatePartner.create_with_web_params(current_user, params) + respond_with_model(@partner) + return + end + else + @partner = AffiliatePartner.create_with_web_params(nil, params) + respond_with_model(@partner) + end + end + + def traffic_index + data = AffiliateTrafficTotal.index(current_user, params) + @traffics, @next = data[0], data[1] + render "api_affiliates/traffic_index", :layout => nil + end + + def monthly_index + data = AffiliateMonthlyPayment.index(current_user, params) + @monthlies, @next = data[0], data[1] + render "api_affiliates/monthly_index", :layout => nil + end + + def quarterly_index + data = AffiliateQuarterlyPayment.index(current_user, params) + @quarterlies, @next = data[0], data[1] + render "api_affiliates/quarterly_index", :layout => nil + end + + def payment_index + data = AffiliatePayment.index(current_user, params) + @payments, @next = data[0], data[1] + render "api_affiliates/payment_index", :layout => nil + end +end diff --git a/web/app/controllers/api_controller.rb b/web/app/controllers/api_controller.rb index ce17dbd9c..7aa20de41 100644 --- a/web/app/controllers/api_controller.rb +++ b/web/app/controllers/api_controller.rb @@ -69,6 +69,18 @@ class ApiController < ApplicationController else auth_user end - end -end \ No newline at end of file + + def affiliate_partner + if params[:affiliate_id] + @partner = AffiliatePartner.find(params[:affiliate_id]) + if @partner.partner_user.nil? + raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR + end + elsif current_user + @partner = current_user.affiliate_partner + else + raise JamPermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR + end + end +end diff --git a/web/app/controllers/api_links_controller.rb b/web/app/controllers/api_links_controller.rb new file mode 100644 index 000000000..9f50d979f --- /dev/null +++ b/web/app/controllers/api_links_controller.rb @@ -0,0 +1,114 @@ +class ApiLinksController < ApiController + + respond_to :json + + before_filter :api_any_user + before_filter :affiliate_partner + + def log + @log || Logging.logger[ApiLinksController] + end + + def jamtrack_song_index + @affiliate_params = affiliate_params('jamtrack-song') + + results = [] + + @jamtracks, @next = JamTrack.index({offset: 0, limit:1000}, any_user) + + @jamtracks.each do |jamtrack| + results << {url: individual_jamtrack_url(jamtrack.short_plan_code, @affiliate_params), + target: "#{jamtrack.original_artist} - #{jamtrack.name}" } + end + + render json: results, status: 200 + end + + def jamtrack_band_index + @affiliate_params = affiliate_params('jamtrack-band') + + results = [] + + @jamtracks, @next = JamTrack.index({offset: 0, limit:1000}, any_user) + @jamtracks.each do |jamtrack| + results << {url: individual_jamtrack_band_url(jamtrack.short_plan_code, @affiliate_params), + target: "#{jamtrack.original_artist} - #{jamtrack.name}" } + end + + render json: results, status: 200 + end + + def jamtrack_general_index + @affiliate_params = affiliate_params('jamtrack-general') + @affiliate_params[:generic] = 1 + + results = [] + + @jamtracks, @next = JamTrack.index({offset: 0, limit:1000}, any_user) + + @jamtracks.each do |jamtrack| + results << {url: individual_jamtrack_url(jamtrack.short_plan_code, @affiliate_params), + target: "#{jamtrack.original_artist} - #{jamtrack.name}" } + end + + render json: results, status: 200 + end + + def jamkazam_general_index + + results = [] + + @affiliate_params = affiliate_params('home') + results << {url: root_url(@affiliate_params), target: 'JamKazam Home'} + + @affiliate_params = affiliate_params('products-jamblaster') + results << {url: product_jamblaster_url(@affiliate_params), target: 'JamBlaster'} + + @affiliate_params = affiliate_params('products-jamtracks') + results << {url: product_jamtracks_url(@affiliate_params), target: 'JamTracks'} + + + render json: results, status: 200 + end + + def session_index + + @affiliate_params = affiliate_params('session') + + results = [] + + ActiveRecord::Base.transaction do + options = {offset:0, limit:50} + @music_sessions = MusicSession.scheduled(@partner.partner_user, true) + + @music_sessions.each do |session| + results << {url: music_scheduled_session_detail_url(session.id, @affiliate_params), target: session.name} + end + end + + render json: results, status: 200 + end + + def recording_index + + @affiliate_params = affiliate_params('recording') + + results = [] + + feeds, next_page = Feed.index(@partner.partner_user, :type => 'recording', time_range: 'all', limit:100, user: @partner.partner_user.id, ) + + feeds.each do |feed| + claim = feed.recording.candidate_claimed_recording + if claim + results << {url: share_token_url(claim.share_token.token, @affiliate_params), target: claim.name} + end + end + + render json: results, status: 200 + end + + private + def affiliate_params(campaign) + {utm_source:'affiliate', utm_medium: 'affiliate', utm_campaign: "#{Date.today.year}-affiliate-#{campaign}", affiliate: @partner.id} + end +end diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb index 366469aad..32b7726fc 100644 --- a/web/app/controllers/api_music_sessions_controller.rb +++ b/web/app/controllers/api_music_sessions_controller.rb @@ -165,7 +165,8 @@ class ApiMusicSessionsController < ApiController def show unless @music_session.can_see? current_user - raise JamRuby::PermissionError + # render :json => { :message => ValidationMessages::PERMISSION_VALIDATION_ERROR }, :status => 403 + raise JamRuby::JamPermissionError end end diff --git a/web/app/controllers/api_recurly_controller.rb b/web/app/controllers/api_recurly_controller.rb index f3090e7a2..41c9c041e 100644 --- a/web/app/controllers/api_recurly_controller.rb +++ b/web/app/controllers/api_recurly_controller.rb @@ -34,7 +34,8 @@ class ApiRecurlyController < ApiController password_confirmation: params[:password], terms_of_service: terms_of_service, location: {:country => billing_info[:country], :state => billing_info[:state], :city => billing_info[:city]}, - reuse_card: reuse_card_next_time + reuse_card: reuse_card_next_time, + affiliate_referral_id: cookies[:affiliate_visitor] } options = User.musician_defaults(request.remote_ip, ApplicationHelper.base_uri(request) + "/confirm", any_user, options) diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index c9210aa00..e8b42df82 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -26,7 +26,7 @@ class ApiUsersController < ApiController @user = User.includes([{musician_instruments: :instrument}, {band_musicians: :user}, {genre_players: :genre}, - :bands, :instruments, :genres, :jam_track_rights]) + :bands, :instruments, :genres, :jam_track_rights, :affiliate_partner]) .find(params[:id]) respond_with @user, responder: ApiResponder, :status => 200 @@ -64,7 +64,8 @@ class ApiUsersController < ApiController password_confirmation: params[:password], terms_of_service: params[:terms], location: {:country => nil, :state => nil, :city => nil}, - signup_hint: signup_hint + signup_hint: signup_hint, + affiliate_referral_id: cookies[:affiliate_visitor] } options = User.musician_defaults(request.remote_ip, ApplicationHelper.base_uri(request) + "/confirm", any_user, options) @@ -722,6 +723,40 @@ class ApiUsersController < ApiController end end + def affiliate_partner + if oo = current_user.affiliate_partner + if request.post? + oo.address = params[:address] + oo.tax_identifier = params[:tax_identifier] + oo.save! + render nothing: true + + elsif request.get? + result = {} + result['account'] = { + 'address' => oo.address.clone, + 'tax_identifier' => oo.tax_identifier, + 'entity_type' => oo.entity_type, + 'partner_name' => oo.partner_name, + 'partner_id' => oo.partner_user_id, + 'id' => oo.id + } + if txt = oo.affiliate_legalese.try(:legalese) + txt = ControllerHelp.instance.simple_format(txt) + end + result['agreement'] = { + 'legalese' => txt, + 'signed_at' => oo.signed_at + } + #result['signups'] = oo.referrals_by_date + #result['earnings'] = [['April 2015', '1000 units', '$100']] + render json: result.to_json, status: 200 + end + else + render :json => { :message => 'user not affiliate partner' }, :status => 400 + end + end + def affiliate_report begin affiliate = User diff --git a/web/app/controllers/application_controller.rb b/web/app/controllers/application_controller.rb index 7f242aff0..114666eec 100644 --- a/web/app/controllers/application_controller.rb +++ b/web/app/controllers/application_controller.rb @@ -15,6 +15,7 @@ class ApplicationController < ActionController::Base end before_filter :set_tracking_cookie + before_filter :track_affiliate_visits before_filter do if params[AffiliatePartner::PARAM_REFERRAL].present? && current_user.nil? @@ -35,6 +36,16 @@ class ApplicationController < ActionController::Base cookies.permanent[:user_uuid] = SecureRandom.uuid unless cookies[:user_uuid] end + def track_affiliate_visits + if params[:affiliate] && params[:utm_medium] == 'affiliate' && params[:utm_source] == 'affiliate' + visit_cookie = cookies[:affiliate_visitor] + AffiliateReferralVisit.track(affiliate_id: params[:affiliate], visited: visit_cookie, remote_ip: request.remote_ip, visited_url: request.fullpath, referral_url: request.referer, current_user: current_user) + + # set a cookie with the ID of the partner, and expires in 24 hours + cookies[:affiliate_visitor] = { :value => params[:affiliate], :expires => Time.now + 3600 * 24} # 1 day from now + end + end + private def add_user_info_to_bugsnag(notif) @@ -49,3 +60,14 @@ class ApplicationController < ActionController::Base end end end + +class ControllerHelp + include Singleton + include ActionView::Helpers::TextHelper + include ActionView::Helpers::UrlHelper + include ActionView::Helpers::SanitizeHelper + extend ActionView::Helpers::SanitizeHelper::ClassMethods + include ActionView::Helpers::JavaScriptHelper + include ActionView::Helpers::TagHelper + include ActionView::Helpers::AssetTagHelper +end diff --git a/web/app/controllers/landings_controller.rb b/web/app/controllers/landings_controller.rb index 5700ac543..ef23ba7d8 100644 --- a/web/app/controllers/landings_controller.rb +++ b/web/app/controllers/landings_controller.rb @@ -103,5 +103,10 @@ class LandingsController < ApplicationController gon.jam_track_plan_code = jam_track.plan_code if jam_track render 'product_jamtracks', layout: 'web' end + + def affiliate_program + render 'affiliate_program', layout: 'web' + end + end diff --git a/web/app/controllers/sessions_controller.rb b/web/app/controllers/sessions_controller.rb index 36c54e58c..931960fdb 100644 --- a/web/app/controllers/sessions_controller.rb +++ b/web/app/controllers/sessions_controller.rb @@ -65,7 +65,8 @@ class SessionsController < ApplicationController user = UserManager.new.signup(remote_ip: remote_ip(), first_name: auth_hash[:info][:first_name], last_name: auth_hash[:info][:last_name], - email: auth_hash[:info][:email]) + email: auth_hash[:info][:email], + affiliate_referral_id: cookies[:affiliate_visitor]) # Users who sign up using oauth are presumed to have valid email adddresses. user.confirm_email! @@ -132,6 +133,7 @@ class SessionsController < ApplicationController email: email, terms_of_service: true, location: {:country => nil, :state => nil, :city => nil}, + affiliate_referral_id: cookies[:affiliate_visitor] } options = User.musician_defaults(request.remote_ip, ApplicationHelper.base_uri(request) + "/confirm", any_user, options) diff --git a/web/app/controllers/users_controller.rb b/web/app/controllers/users_controller.rb index 47675f9ce..92fbb1f52 100644 --- a/web/app/controllers/users_controller.rb +++ b/web/app/controllers/users_controller.rb @@ -70,6 +70,7 @@ class UsersController < ApplicationController end end + @affiliate_partner = load_affiliate_partner(params) @invited_user = load_invited_user(params) if !@invited_user.nil? && @invited_user.has_required_email? && @invited_user.accepted @@ -77,7 +78,7 @@ class UsersController < ApplicationController render "already_signed_up", :layout => 'landing' return end - @signup_postback = load_postback(@invited_user, @fb_signup) + @signup_postback = load_postback(@invited_user, @fb_signup, @affiliate_partner) load_location(request.remote_ip) @@ -136,7 +137,8 @@ class UsersController < ApplicationController end @invited_user = load_invited_user(params) - @signup_postback = load_postback(@invited_user, @fb_signup) + @affiliate_partner = load_affiliate_partner(params) + @signup_postback = load_postback(@invited_user, @fb_signup, @affiliate_partner) instruments = fixup_instruments(params[:jam_ruby_user][:instruments]) @@ -161,7 +163,8 @@ class UsersController < ApplicationController invited_user: @invited_user, fb_signup: @fb_signup, signup_confirm_url: ApplicationHelper.base_uri(request) + "/confirm", - affiliate_referral_id: AffiliatePartner.coded_id(self.affiliate_code)) + affiliate_referral_id: cookies[:affiliate_visitor], + affiliate_partner: @affiliate_partner) # check for errors if @user.errors.any? @@ -196,9 +199,14 @@ class UsersController < ApplicationController render :layout => "web" end - # DO NOT USE CURRENT_USER IN THIS ROUTINE. IT'S CACHED FOR THE WHOLE SITE + # DO NOT USE CURRENT_USER IN THIS ROUTINE UNLESS REDIRECTING. IT'S CACHED FOR THE WHOLE SITE def home + if current_user + redirect_to "/client#/home" + return + end + @no_user_dropdown = false @promo_buzz = PromoBuzz.active @@ -209,7 +217,6 @@ class UsersController < ApplicationController end - gon.signed_in = !current_user.nil? render :layout => "web" end @@ -464,6 +471,12 @@ JS return invited_user end + def load_affiliate_partner(params) + partner_id = params[:affiliate_partner_id] + + AffiliatePartner.find(partner_id) if partner_id + end + def load_location(remote_ip, location = nil) # useful if you need to repro something on 127.0.0.1 # remote_ip = ' 23.119.29.89' @@ -482,10 +495,11 @@ JS @cities = @location[:state].nil? ? [] : MaxMindManager.cities(@location[:country], @location[:state]) end - def load_postback(invited_user, fb_signup) + def load_postback(invited_user, fb_signup, affiliate_partner) query = {} query[:invitation_code] = invited_user.invitation_code if invited_user query[:facebook_signup] = fb_signup.lookup_id if fb_signup + query[:affiliate_partner_id] = affiliate_partner.id if affiliate_partner if query.length > 0 signup_path + "?" + query.to_query else diff --git a/web/app/helpers/errors_helper.rb b/web/app/helpers/errors_helper.rb new file mode 100644 index 000000000..bb97b5367 --- /dev/null +++ b/web/app/helpers/errors_helper.rb @@ -0,0 +1,7 @@ +module ErrorsHelper + + def simple_error(field, error_msg) + {"errors" => {field => [error_msg]}} + end + +end \ No newline at end of file diff --git a/web/app/helpers/sessions_helper.rb b/web/app/helpers/sessions_helper.rb index a5e914512..8ce8f71f2 100644 --- a/web/app/helpers/sessions_helper.rb +++ b/web/app/helpers/sessions_helper.rb @@ -22,7 +22,6 @@ module SessionsHelper end end - # should be set whenever a user logs in who has redeemed a free jamtrack, or whenever the user def set_purchased_jamtrack_cookie cookies.permanent[:redeemed_jamtrack] = true diff --git a/web/app/views/affiliates/affiliate_links.html.slim b/web/app/views/affiliates/affiliate_links.html.slim new file mode 100644 index 000000000..c86d86fb2 --- /dev/null +++ b/web/app/views/affiliates/affiliate_links.html.slim @@ -0,0 +1,119 @@ +- provide(:page_name, 'landing_page full landing_product affiliate_links') +- provide(:description, 'Affiliate Links') + +h2 Affiliate Links +p.intro + | This page provides you with lists of ready-to-use links you can use to direct your followers to JamKazam.  + | These links include your unique affiliate ID, and users who follow these links to JamKazam and register for an account will be  + | tagged with your affiliate ID, so that you will be paid on their purchase per the terms of the affiliate agreement. + +h3 Table of Contents +ul + li + a href='#header_jamtrack_songs' JamTrack Songs + li + a href='#header_jamtrack_bands' JamTrack Bands + li + a href='#header_jamtrack_general' JamTrack Generic + li + a href='#header_jamkazam' JamKazam General + li + a href='#header_sessions' Sessions + li + a href='#header_recordings' Recordings + li + a href='#header_custom_links' Custom Links +h3#header_jamtrack_songs JamTrack Songs +.link-type-prompt data-type='jamtrack_songs' + | These links take users directly to the "landing page" for a JamTrack - i.e. an individual piece of music. This would be a great fit, for example, if you have produced a YouTube tutorial on how to play a particular song, or if you have music notation for a particular song, etc. +table.links-table.jamtrack_songs + thead + tr + th.target TARGET PAGE + th.copy-link + th.url URL + tbody + +h3#header_jamtrack_bands JamTrack Bands +.link-type-prompt data-type='jamtrack_bands' + | These links take users directly to the "landing page" for a band to promote all the JamTracks by that band, not just an individual song. This would be a great fit, for example, if you have music notation for several songs by a band, etc. +table.links-table.jamtrack_bands + thead + tr + th.target TARGET PAGE + th.copy-link + th.url URL + tbody + +h3#header_jamtrack_general JamTrack General +.link-type-prompt data-type='jamtrack_general' + | These links take users directly to a"landing page" that promotes JamTracks in general. This page will feature the song listed in the link as an example of a JamTrack, but the landing page is built to promote JamTracks in general versus promoting just the one song. This is a good fit if you want to let your followers know about JamTracks in general. +table.links-table.jamtrack_general + thead + tr + th.target TARGET PAGE + th.copy-link + th.url URL + tbody + +h3#header_jamkazam JamKazam Generic +.link-type-prompt data-type='jamkazam' + | These links take users directly to a product page that promotes JamKazam more generally. This is a good fit if you want to let your followers know about JamKazam or its products in general. +table.links-table.jamkazam + thead + tr + th.target TARGET PAGE + th.copy-link + th.url URL + tbody + +h3#header_sessions Sessions +.link-type-prompt data-type='sessions' + | These links take users to a page where they can listen to a session you have organized, and in which you are scheduled to perform, either alone or with others. This is a good fit if you can perform either solo or in a group with other musicians in a JamKazam session, and draw an audience to listen to your session. During the session, you can recommend that your audience members sign up for JamKazam from this page. Below is a list of your currently scheduled JamKazam sessions for which you may share links. +table.links-table.sessions + thead + tr + th.target TARGET PAGE + th.copy-link + th.url URL + tbody + +h3#header_recordings Recordings +.link-type-prompt data-type='recordings' + | These links take users to a page with a recording you have made in a JamKazam session. This is a good fit if you have made a nice recording in a JamKazam session and can share it with your followers via Facebook, Twitter, email, and so on. Below is a list of your most recent JamKazam recordings for which you may share links. +table.links-table.recordings + thead + tr + th.target TARGET PAGE + th.copy-link + th.url URL + tbody + +h3#header_custom_links Custom Links +.link-type-prompt data-type='custom_links' + p You may also link to any page on the JamKazam website and append the following text to the URL of the page to which you are linking: + + p.example-link + | ?utm_source=affiliate&utm_medium=affiliate&utm_campaign=2015-affiliate-custom&affiliate= + span.affiliate_id = @partner.id + + p For example, if you were linking to the JamKazam home page, you would combine https://www.jamkazam.com with the text above, and the result would be: + + p.example-link + | https://www.jamkazam.com?utm_source=affiliate&utm_medium=affiliate&utm_campaign=2015-affiliate-custom&affiliate= + span.affiliate_id = @partner.id + +javascript: + $(document).on('JAMKAZAM_READY', function (e, data) { + var affiliateLinks = new JK.AffiliateLinks(data.app, '#{@partner.id}'); + affiliateLinks.initialize(); + }) + +script type="text/template" id="template-affiliate-link-row" + tr + td.target + | {{data.target}} + td.copy-link + a href='#' select link + td.url + input type="text" value="{{data.url}}" \ No newline at end of file diff --git a/web/app/views/api_affiliates/create.rabl b/web/app/views/api_affiliates/create.rabl new file mode 100644 index 000000000..bd016e7bc --- /dev/null +++ b/web/app/views/api_affiliates/create.rabl @@ -0,0 +1,3 @@ +object @partner + +extends "api_affiliates/show" \ No newline at end of file diff --git a/web/app/views/api_affiliates/monthly_index.rabl b/web/app/views/api_affiliates/monthly_index.rabl new file mode 100644 index 000000000..3446d43ea --- /dev/null +++ b/web/app/views/api_affiliates/monthly_index.rabl @@ -0,0 +1,8 @@ +node :next do |page| + @next +end + +node :monthlies do |page| + partial "api_affiliates/monthly_show", object: @monthlies +end + diff --git a/web/app/views/api_affiliates/monthly_show.rabl b/web/app/views/api_affiliates/monthly_show.rabl new file mode 100644 index 000000000..7efad0339 --- /dev/null +++ b/web/app/views/api_affiliates/monthly_show.rabl @@ -0,0 +1,3 @@ +object @monthly + +attribute :closed, :due_amount_in_cents, :affiliate_partner_id, :month, :year \ No newline at end of file diff --git a/web/app/views/api_affiliates/payment_index.rabl b/web/app/views/api_affiliates/payment_index.rabl new file mode 100644 index 000000000..505f0d0e0 --- /dev/null +++ b/web/app/views/api_affiliates/payment_index.rabl @@ -0,0 +1,8 @@ +node :next do |page| + @next +end + +node :payments do |page| + partial "api_affiliates/payment_show", object: @payments +end + diff --git a/web/app/views/api_affiliates/payment_show.rabl b/web/app/views/api_affiliates/payment_show.rabl new file mode 100644 index 000000000..1ac51e258 --- /dev/null +++ b/web/app/views/api_affiliates/payment_show.rabl @@ -0,0 +1,3 @@ +object @quarterly + +attribute :closed, :paid, :due_amount_in_cents, :affiliate_partner_id, :quarter, :month, :year, :payment_type, :jamtracks_sold \ No newline at end of file diff --git a/web/app/views/api_affiliates/quarterly_index.rabl b/web/app/views/api_affiliates/quarterly_index.rabl new file mode 100644 index 000000000..a492bfa6a --- /dev/null +++ b/web/app/views/api_affiliates/quarterly_index.rabl @@ -0,0 +1,8 @@ +node :next do |page| + @next +end + +node :quarterlies do |page| + partial "api_affiliates/quarterly_show", object: @quarterlies +end + diff --git a/web/app/views/api_affiliates/quarterly_show.rabl b/web/app/views/api_affiliates/quarterly_show.rabl new file mode 100644 index 000000000..6238f5bc6 --- /dev/null +++ b/web/app/views/api_affiliates/quarterly_show.rabl @@ -0,0 +1,3 @@ +object @quarterly + +attribute :closed, :paid, :due_amount_in_cents, :affiliate_partner_id, :quarter, :year \ No newline at end of file diff --git a/web/app/views/api_affiliates/show.rabl b/web/app/views/api_affiliates/show.rabl new file mode 100644 index 000000000..837dc9eed --- /dev/null +++ b/web/app/views/api_affiliates/show.rabl @@ -0,0 +1,2 @@ + +attributes :id, :partner_user_id, :partner_name, :entity_type \ No newline at end of file diff --git a/web/app/views/api_affiliates/traffic_index.rabl b/web/app/views/api_affiliates/traffic_index.rabl new file mode 100644 index 000000000..f5647a946 --- /dev/null +++ b/web/app/views/api_affiliates/traffic_index.rabl @@ -0,0 +1,8 @@ +node :next do |page| + @next +end + +node :traffics do |page| + partial "api_affiliates/traffic_show", object: @traffics +end + diff --git a/web/app/views/api_affiliates/traffic_show.rabl b/web/app/views/api_affiliates/traffic_show.rabl new file mode 100644 index 000000000..e60d6da02 --- /dev/null +++ b/web/app/views/api_affiliates/traffic_show.rabl @@ -0,0 +1,3 @@ +object @traffic + +attribute :day, :visits, :signups, :affiliate_partner_id \ No newline at end of file diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl index 0c217486b..412b38a6f 100644 --- a/web/app/views/api_users/show.rabl +++ b/web/app/views/api_users/show.rabl @@ -34,6 +34,18 @@ if @user == current_user @user.recurly_code == @user.id end + node :is_affiliate_partner do + @user.affiliate_partner.present? + end + + node :affiliate_referral_count do + @user.affiliate_partner.try(:referral_user_count) + end + + node :affiliate_earnings do + @user.affiliate_partner.try(:cumulative_earnings_in_cents) + end + elsif current_user node :is_friend do |uu| current_user.friends?(@user) diff --git a/web/app/views/clients/_account.html.erb b/web/app/views/clients/_account.html.erb index af5e85f10..29d806a94 100644 --- a/web/app/views/clients/_account.html.erb +++ b/web/app/views/clients/_account.html.erb @@ -151,6 +151,35 @@
    {% } %} + +
    + + + + {% if (data.is_affiliate_partner) { %} + +
    + VIEW +
    + {% } else { %} + + {% } %} +
    +
    diff --git a/web/app/views/clients/_account_affiliate_partner.html.slim b/web/app/views/clients/_account_affiliate_partner.html.slim new file mode 100644 index 000000000..8280cf9b9 --- /dev/null +++ b/web/app/views/clients/_account_affiliate_partner.html.slim @@ -0,0 +1,193 @@ +/! Account affiliates Dialog +#account-affiliate-partner.screen.secondary layout='screen' layout-id='account/affiliatePartner' + .content-head + .content-icon + = image_tag "content/icon_account.png", :width => 27, :height => 20 + h1 my account + = render "screen_navigation" + + /! affiliates scrolling area + .content-body + .content-body-scroller.account-content-scroller#account-affiliate-partner-content-scroller + .content-wrapper.account-affiliates + .affiliates-header + .left.affiliates-caption + h2 affiliate: + .clearall + .affiliate-partner-nav + a#affiliate-partner-agreement-link agreement + a#affiliate-partner-earnings-link earnings + a#affiliate-partner-signups-link signups + a#affiliate-partner-links-link links + a#affiliate-partner-account-link.active account + .clearall + + .affiliate-partner-account + .clearall + .affiliate-partner-links + .clearall + .affiliate-partner-agreement + .clearall + .affiliate-partner-signups + .clearall + .affiliate-partner-earnings + .clearall + #affiliate-partner-tab-content + + .clearall + +script type="text/template" id="template-affiliate-partner-account" + .tab-account + .right-col + | We must have a complete mailing address and a valid tax ID in order to process and mail payments due to all affiliates.  Per the terms of the affiliate agreement, if this information is not available within 90 days of the end of a calendar quarter, then any payment obligation due to the affiliate for such calendar quarter shall be considered fully and permanently discharged, and no further payment for such calendar quarter shall be due or payable to affiliate. + br + | So please provide this data, and be sure to keep it current! + + .left-col + .affiliate-label Affiliate: + .w80= "{{ data.partner_name }} ({{data.entity_type}})" + br + .affiliate-label Street Address 1: + = text_field_tag('affiliate_partner_address1', "{{ data.address.address1 }}", {class: 'w60'}) + br + .affiliate-label Street Address 2: + = text_field_tag('affiliate_partner_address2', "{{ data.address.address2 }}", {class: 'w60'}) + br + .affiliate-label City: + = text_field_tag('affiliate_partner_city', "{{ data.address.city }}", {class: 'w60'}) + br + .affiliate-label State/Region: + = text_field_tag('affiliate_partner_state', "{{ data.address.state }}", {class: 'w60'}) + br + .affiliate-label Zip/Postal Code: + = text_field_tag('affiliate_partner_postal_code', "{{ data.address.postal_code }}", {class: 'w60'}) + br + .affiliate-label Country: + = text_field_tag('affiliate_partner_country', "{{ data.address.country }}", {class: 'w60'}) + br + br + .affiliate-label Tax ID: + = text_field_tag('affiliate_partner_tax_identifier', "{{ data.tax_identifier }}", {class: 'w60'}) + br + .spacer + .input-buttons + .right + a.button-grey href="javascript:history.go(-1)" CANCEL + a.button-orange id="affiliate-profile-account-submit" href="#" UPDATE + .clearall + .clearall + +script type="text/template" id="template-affiliate-partner-agreement" + .agreement + label.partner-agreement You are viewing: + select.easydropdown name='agreement' + option value="Current agreement in effect" Current agreement in effect + br + h2 JamKazam Affiliate Agreement + .execution-date + | (you executed this Agreement on {{ window.JK.formatDate(data.signed_at) }}) + + = render "legal/partner_agreement_v1" + + .input-buttons + .right + a.button-orange href='/client#/account' BACK TO ACCOUNT + +script type="text/template" id="template-affiliate-partner-signups" + table.traffic-table.jamtable + thead + tr + th.day DATE + th.signups SIGNUPS + th.visits VISITS + tbody + +script type="text/template" id="template-affiliate-partner-links" + .links + p.prompt + | This page provides you with lists of ready-to-use links you can use to direct your followers to JamKazam.  + | These links include your unique affiliate ID, and users who follow these links to JamKazam and register for an account will be  + | tagged with your affiliate ID, so that you will be paid on their purchase per the terms of the  + a.affiliate-agreement rel='#' affiliate agreement + | . You can also find a  + a.affiliate-link-page href='#' rel="external" single page listing all affiliate links + |  which you can share with others who may need to share out affiliate links on your behalf. + .link-type-section + label Link Type: + select.link_type.easydropdown name="link_type" + option value="JamTrack Song" JamTrack Song + option value="JamTrack Band" JamTrack Band + option value="JamTrack General" JamTrack General + option value="JamKazam General" JamKazam General + option value="JamKazam Session" JamKazam Session + option value="JamKazam Recording" JamKazam Recording + option value="Custom Link" Custom Link + .link-type-prompt data-type='jamtrack_songs' + | These links take users directly to the "landing page" for a JamTrack - i.e. an individual piece of music. This would be a great fit, for example, if you have produced a YouTube tutorial on how to play a particular song, or if you have music notation for a particular song, etc. + .link-type-prompt data-type='jamtrack_bands' + | These links take users directly to the "landing page" for a band to promote all the JamTracks by that band, not just an individual song. This would be a great fit, for example, if you have music notation for several songs by a band, etc. + .link-type-prompt data-type='jamtrack_general' + | These links take users directly to a"landing page" that promotes JamTracks in general. This page will feature the song listed in the link as an example of a JamTrack, but the landing page is built to promote JamTracks in general versus promoting just the one song. This is a good fit if you want to let your followers know about JamTracks in general. + .link-type-prompt data-type='jamkazam' + | These links take users directly to a product page that promotes JamKazam more generally. This is a good fit if you want to let your followers know about JamKazam or its products in general. + .link-type-prompt data-type='sessions' + | These links take users to a page where they can listen to a session you have organized, and in which you are scheduled to perform, either alone or with others. This is a good fit if you can perform either solo or in a group with other musicians in a JamKazam session, and draw an audience to listen to your session. During the session, you can recommend that your audience members sign up for JamKazam from this page. Below is a list of your currently scheduled JamKazam sessions for which you may share links. + .link-type-prompt data-type='recordings' + | These links take users to a page with a recording you have made in a JamKazam session. This is a good fit if you have made a nice recording in a JamKazam session and can share it with your followers via Facebook, Twitter, email, and so on. Below is a list of your most recent JamKazam recordings for which you may share links. + .link-type-prompt data-type='custom_links' + p You may also link to any page on the JamKazam website and append the following text to the URL of the page to which you are linking: + + p.example-link + | ?utm_source=affiliate&utm_medium=affiliate&utm_campaign=2015-affiliate-custom&affiliate= + span.affiliate_id + + p For example, if you were linking to the JamKazam home page, you would combine https://www.jamkazam.com with the text above, and the result would be: + + p.example-link + | https://www.jamkazam.com?utm_source=affiliate&utm_medium=affiliate&utm_campaign=2015-affiliate-custom&affiliate= + span.affiliate_id + + table.links-table + thead + tr + th.target TARGET PAGE + th.copy-link + th.url URL + tbody + +script type="text/template" id="template-affiliate-partner-signups-row" + tr + td.day + | {{data.day}} + td.signups + | {{data.signups}} + td.visits + | {{data.visits}} + +script type="text/template" id="template-affiliate-partner-earnings" + table.payment-table.jamtable + thead + tr + th.month MONTH + th.sales SALES + th.earnings AFFILIATE EARNINGS + tbody + +script type="text/template" id="template-affiliate-partner-earnings-row" + tr data-type="{{data.payment_type}}" + td.month + | {{data.time}} + td.sales + | {{data.sold}} + td.earnings + | {{data.earnings}} + + +script type="text/template" id="template-affiliate-link-entry" + tr + td.target + | {{data.target}} + td.copy-link + a href='#' select link + td.url + input type="text" value="{{data.url}}" \ No newline at end of file diff --git a/web/app/views/clients/_help.html.slim b/web/app/views/clients/_help.html.slim index dac0816e4..c81528e9c 100644 --- a/web/app/views/clients/_help.html.slim +++ b/web/app/views/clients/_help.html.slim @@ -286,3 +286,27 @@ script type="text/template" id="template-help-band-profile-play-paid-gigs" script type="text/template" id="template-help-band-profile-play-free-gigs" | For bands that are interested in playing free gigs, either for fun or to build experience. + +script type="text/template" id="template-help-jamtrack-landing-preview" + .jamtrack-landing-preview.big-help + p click a play button to preview the master mix and individual tracks of the JamTrack + +script type="text/template" id="template-help-jamtrack-landing-video" + .jamtrack-landing-video.big-help + p click to watch JamTracks video + +script type="text/template" id="template-help-jamtrack-landing-cta" + .jamtrack-landing-cta.big-help + p click to browse through our JamTracks collection and get your first one free! + +script type="text/template" id="template-help-jamtrack-browse-band" + .jamtrack-browse-band.big-help + p List JamTracks of a specified band + +script type="text/template" id="template-help-jamtrack-browse-master-mix" + .jamtrack-browse-master-mix.big-help + p Listen to master mix and individual tracks + +script type="text/template" id="template-help-jamtrack-browse-cta" + .jamtrack-browse-cta.big-help + p Click to select your first free JamTrack! diff --git a/web/app/views/clients/_jam_track_preview.html.slim b/web/app/views/clients/_jam_track_preview.html.slim index 1fe91fb33..bf6f22d40 100644 --- a/web/app/views/clients/_jam_track_preview.html.slim +++ b/web/app/views/clients/_jam_track_preview.html.slim @@ -6,4 +6,5 @@ script type="text/template" id='template-jam-track-preview' img.instrument-icon hoveraction="instrument" data-instrument-id="" width="24" height="24" .instrument-name .part - .loading.spinner-small.hidden \ No newline at end of file + .loading.spinner-small.hidden + .loading-text.hidden preview loading \ No newline at end of file diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index 128a693d1..c41ae70bc 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -63,6 +63,7 @@ <%= render "account_video_profile" %> <%= render "account_sessions" %> <%= render "account_jamtracks" %> +<%= render "account_affiliate_partner" %> <%= render "account_session_detail" %> <%= render "account_session_properties" %> <%= render "account_payment_history" %> @@ -217,6 +218,9 @@ var accountSessionPropertiesScreen = new JK.AccountSessionProperties(JK.app); accountSessionPropertiesScreen.initialize(); + var accountAffiliateScreen = new JK.AccountAffiliateScreen(JK.app); + accountAffiliateScreen.initialize(); + var affiliateReportScreen = new JK.AffiliateReportScreen(JK.app); affiliateReportScreen.initialize(); diff --git a/web/app/views/landings/affiliate_program.html.slim b/web/app/views/landings/affiliate_program.html.slim new file mode 100644 index 000000000..34859ff76 --- /dev/null +++ b/web/app/views/landings/affiliate_program.html.slim @@ -0,0 +1,56 @@ +- provide(:page_name, 'landing_page full landing_product affiliate_program') +- provide(:description, 'Signup for JamKazam Affiliate Program') + +.row + .column + h1.product-headline JamKazam Affiliate Program + p Do you have a following of musicians on Facebook, YouTube, Twitter or an email list? Generate income simply by letting them know about JamKazam. + p Let's say you make YouTube tutorial videos. You can link directly from a video on how to play "Back in Black" to our JamTrack for that song. Video watchers can get this first JamTrack free, and can buy others if they like. You get paid every time they buy something from us for 2 years. + p Or let's say you have a Facebook group for guitarist with 5,000 members. You can let them know they can play together free on JamKazam. For everyone who signs up, you get paid every time they buy something from us for 2 years. + p You don't have to sell anything. Just let your followers know about cool new stuff they'll like! To get started, simply review the affiliate agreement below, accept it (at the end of the agreement), and then start sharing links with your affiliate code. When referred users buy JamTracks, JamBlasters, JamLessons, and so on, you get paid! + .column + h1 Learn How to Make Money by Referring Users + .video-wrapper + .video-container + iframe src="//www.youtube.com/embed/ylYcvTY9CVo" frameborder="0" allowfullscreen + br clear="all" +.row + h1 JamKazam Affiliate Agreement + = render "legal/partner_agreement_v1" + + p.agreement-notice By clicking the "I Agree" button below, I certify that I have the authority to enter into this Agreement on behalf of myself as an individual or on behalf of the entity I have listed below, and I further certify that I have read, understood, and agree to be bound by the terms above. + .entity-options + .field.radio + = radio_button_tag(:entity, 'individual') + label I am entering into this Agreement as an individual + .field.radio + = radio_button_tag(:entity, 'entity') + label I am executing this Agreement on behalf of the company or entity listed below + + .entity-info.hidden + .field.entity.name + label Entity Name + input type="text" name="entity-name" + .field.entity.type + label Entity Type + select name="entity-type" + option value="" Choose Entity Type + option value="Sole Proprietor" Sole Proprietor + option value="Limited Liability Company (LLC)" Limited Liability Company (LLC) + option value="Partnership" Partnership + option value="Trust/Estate" Trust/Estate + option value="S Corporation" S Corporation + option value="C Corporation" C Corporation + option value="Other" Other + + .agree-disagree-buttons + = link_to image_tag("content/agree_button.png", {:width => 213, :height => 50 }), '#', class: "agree-button" + = link_to image_tag("content/disagree_button.png", {:width => 213, :height => 50 }), '#', class: "disagree-button" + p.disagree-text.hidden + | Thank you for your interest in the JamKazam affiliate program. We are sorry, but you cannot join the program without consenting to the terms of this Agreement. + +javascript: + $(document).on('JAMKAZAM_READY', function(e, data) { + var affiliateProgram = new JK.AffiliateProgram(data.app); + affiliateProgram.initialize(); + }) \ No newline at end of file diff --git a/web/app/views/landings/individual_jamtrack.html.slim b/web/app/views/landings/individual_jamtrack.html.slim index c3b6e9daa..cb1cfc771 100644 --- a/web/app/views/landings/individual_jamtrack.html.slim +++ b/web/app/views/landings/individual_jamtrack.html.slim @@ -1,5 +1,6 @@ - provide(:page_name, 'landing_page full landing_jamtrack individual_jamtrack') - provide(:description, @jam_track.nil? ? nil : "Preview multi-track JamTrack recording: #{@jam_track.name} by #{@jam_track.original_artist}. Way better than a backing track.") +- provide(:title, @jam_track.nil? ? nil : "Preview JamTrack: #{@jam_track.name} by #{@jam_track.original_artist}") .one_by_two .row diff --git a/web/app/views/landings/individual_jamtrack_band.html.slim b/web/app/views/landings/individual_jamtrack_band.html.slim index 88237cd56..9b2b72b19 100644 --- a/web/app/views/landings/individual_jamtrack_band.html.slim +++ b/web/app/views/landings/individual_jamtrack_band.html.slim @@ -1,5 +1,6 @@ - provide(:page_name, 'landing_page full landing_jamtrack individual_jamtrack_band') - provide(:description, @jam_track.nil? ? nil : "Preview multi-track JamTrack recording: #{@jam_track.name} by #{@jam_track.original_artist}. Way better than a backing track.") +- provide(:title, @jam_track.nil? ? nil : "Preview JamTrack: #{@jam_track.name} by #{@jam_track.original_artist}") .one_by_two .row diff --git a/web/app/views/layouts/application.html.erb b/web/app/views/layouts/application.html.erb index 378a7356c..0f988cd57 100644 --- a/web/app/views/layouts/application.html.erb +++ b/web/app/views/layouts/application.html.erb @@ -6,15 +6,6 @@ - - <%= stylesheet_link_tag "client/ie", media: "all" %> <%= stylesheet_link_tag "client/jamkazam", media: "all" %> @@ -32,10 +23,6 @@ <%= stylesheet_link_tag "client/search", media: "all" %> <%= stylesheet_link_tag "client/ftue", media: "all" %> <%= stylesheet_link_tag "client/createSession", media: "all" %> - <% if bugsnag? %> - - - <% end %> <%= include_gon %> <%= csrf_meta_tags %> diff --git a/web/app/views/layouts/client.html.erb b/web/app/views/layouts/client.html.erb index dd7d51a9b..6c234aba7 100644 --- a/web/app/views/layouts/client.html.erb +++ b/web/app/views/layouts/client.html.erb @@ -6,23 +6,8 @@ - - <%= stylesheet_link_tag "client/client", media: "all" %> - <% if bugsnag? %> - - - <% end %> <%= include_gon %> <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> diff --git a/web/app/views/layouts/corporate.html.erb b/web/app/views/layouts/corporate.html.erb index 763a78c93..1d1f31f41 100644 --- a/web/app/views/layouts/corporate.html.erb +++ b/web/app/views/layouts/corporate.html.erb @@ -2,24 +2,9 @@ <%= full_title(yield(:title)) %> - - <%= stylesheet_link_tag "corp/corporate", :media => "all" %> - <% if bugsnag? %> - - - <% end %> <%= include_gon(:init => true) %> <%= javascript_include_tag "corp/corporate" %> <%= csrf_meta_tags %> diff --git a/web/app/views/layouts/landing.html.erb b/web/app/views/layouts/landing.html.erb index f3b891b82..13f816530 100644 --- a/web/app/views/layouts/landing.html.erb +++ b/web/app/views/layouts/landing.html.erb @@ -6,23 +6,8 @@ - - <%= stylesheet_link_tag "landing/landing", media: "all" %> - <% if bugsnag? %> - - - <% end %> <%= include_gon(:init => true) %> <%= csrf_meta_tags %> diff --git a/web/app/views/layouts/minimal.html.erb b/web/app/views/layouts/minimal.html.erb index 37bf88c9e..411b792c5 100644 --- a/web/app/views/layouts/minimal.html.erb +++ b/web/app/views/layouts/minimal.html.erb @@ -6,23 +6,8 @@ - - <%= stylesheet_link_tag "minimal/minimal", media: "all" %> - <% if bugsnag? %> - - - <% end %> <%= include_gon(:init => true) %> <%= csrf_meta_tags %> diff --git a/web/app/views/layouts/web.html.erb b/web/app/views/layouts/web.html.erb index f64eb8d59..19f8fd91b 100644 --- a/web/app/views/layouts/web.html.erb +++ b/web/app/views/layouts/web.html.erb @@ -6,23 +6,8 @@ - - <%= stylesheet_link_tag "web/web", media: "all" %> - <% if bugsnag? %> - - - <% end %> <%= include_gon(:init => true) %> <%= csrf_meta_tags %> diff --git a/web/app/views/legal/_partner_agreement_v1.html.erb b/web/app/views/legal/_partner_agreement_v1.html.erb new file mode 100644 index 000000000..1dbc81472 --- /dev/null +++ b/web/app/views/legal/_partner_agreement_v1.html.erb @@ -0,0 +1,422 @@ + +
    + +

    Updated: April 30, 2015.

    + +

    + +

    + This Affiliate Agreement (this “Agreement”) contains the terms and conditions that govern your participation in the JamKazam affiliate marketing program (the “Program”). “JamKazam”, “we,” “us,” + or “our” + means JamKazam, Inc. “You” + or “your” + means the applicant. A “site” + means a website. “JamKazam Site” means the jamkazam.com website or a JamKazam applicaion or any other site that is owned or operated by or on behalf of us and which is identified as participating in the Program in the Program Advertising Fee Schedule in Section 10, as applicable. “Your Site” + means any site(s), software application(s), or content that you create, own, or operate and link to the JamKazam Site. +

    + +

    + +

    1. Description of the Program

    + +

    + +

    + The purpose of the Program is to permit you to advertise Products on Your Site and to earn advertising fees for Qualifying Purchases (defined in Section 7) made by your Qualifying Customers (defined in Section 7). A “Product” + is an item sold on the JamKazam Site and listed in the Program Advertising Fee Schedule in Section 10. In order to facilitate your advertisement of Products, we may make available to you data, images, text, link formats, widgets, links, and other linking tools, and other information in connection with the Program (“Content”). +

    + +

    + +

    2. Enrollment, Suitability, and Communications

    + +

    + +

    To enroll in the Program, you must execute this Agreement by clicking the “I Agree” + button at the end of this Agreement, after having thoroughly reviewed the Agreement.

    + +

    + +

    + Your Site may be considered unsuitable for participation in the Program resulting in termination of this Agreement by JamKazam if Your Site: +

    + +

    +
      +
    • promotes or contains sexually explicit materials;
    • +
    • promotes violence or contains violent materials;
    • +
    • promotes or contains libelous or defamatory materials;
    • +
    • + promotes discrimination, or employs discriminatory practices, based on race, sex, religion, nationality, disability, sexual orientation, or age; +
    • +
    • promotes or undertakes illegal activities;
    • +
    • + is directed toward children under 13 years of age, as defined by the Children’s Online Privacy Protection Act (15 U.S.C. §§ + 6501-6506) and any regulations promulgated thereunder;
    • +
    • + includes any trademark of JamKazam or a variant or misspelling of a trademark of JamKazam in any domain name, subdomain name, or in any username, group name, or other identifier on any social networking site; or +
    • +
    • otherwise violates intellectual property rights.
    • +
    +

    + +

    + You will ensure that the information you provide in executing this Agreement and otherwise associated with your account, including your name, email address, mailing address, tax ID, and other information, is at all times complete, accurate, and up-to-date. We may send notifications (if any), reports (if any), and other communications relating to the Program and this Agreement to the email address then-currently associated with your JamKazam account. You will be deemed to have received all notifications, reports, and other communications sent to that email address, even if the email address associated with your account is no longer current. We may mail checks (if any) payable to you for advertising fees earned under this Agreement to the mailing address then-currently associated with your JamKazam account. You will be deemed to have received all checks sent to that mailing address, even if the mailing address associated with your account is no longer current. If we send a check to a mailing address that is no longer valid, we may, in our sole discretion, choose to issue a stop payment order for such a check and send a new check to a new current address that your provide, but in such a case, we will charge a US$50.00 fee for this exception process. +

    + +

    + +

    + If you are a Non-US person participating in the Program, you agree that you will perform all services under the Agreement outside the United States. +

    + +

    + +

    3. Links on Your Site

    + +

    + +

    + After you have entered into this Agreement, you may display Special Links on Your Site. “Special Links” + are links to the JamKazam Site that you place on Your Site in accordance with this Agreement, that properly utilize the special “tagged” + link formats we specify or provide. Special Links permit accurate tracking, reporting, and accrual of advertising fees. +

    + +

    + +

    + You may earn advertising fees only as described in Section 7 and only with respect to activity on the JamKazam Site occurring directly through Special Links. We will have no obligation to pay you advertising fees if you fail to properly format the links on Your Site to the JamKazam Site as Special Links, including to the extent that such failure may result in any reduction of advertising fee amounts that would otherwise be paid to you under this Agreement. +

    + +

    + +

    4. Program Requirements

    + +

    + +

    You hereby consent to us:

    + +

    +
      +
    • sending you emails relating to the Program from time to time; and
    • +
    • + monitoring, recording, using, and disclosing information about Your Site and visitors to Your Site that we obtain in connection with your display of Special Links in accordance with the JamKazam Privacy Policy. +
    • +
    +

    + +

    5. Responsibility for Your Site

    + +

    + +

    + You will be solely responsible for Your Site including its development, operation, and maintenance, and all materials that appear on or within it. For example, you will be solely responsible for: +

    + +

    +
      +
    • the technical operation of Your Site and all related equipment;
    • +
    • + displaying Special Links and Content on Your Site in compliance with this Agreement and any agreement between you and any other person or entity (including any restrictions or requirements placed on you by any person or entity that hosts Your Site); +
    • +
    • + creating and posting, and ensuring the accuracy, completeness, and appropriateness of, materials posted on Your Site (including all Product descriptions and other Product-related materials and any information you include within or associate with Special Links); +
    • +
    • + using the Content, Your Site, and the materials on or within Your Site in a manner that does not infringe, violate, or misappropriate any of our rights or those of any other person or entity (including copyrights, trademarks, privacy, publicity or other intellectual property or proprietary rights); +
    • +
    • + disclosing on Your Site accurately and adequately, either through a privacy policy or otherwise, how you collect, use, store, and disclose data collected from visitors, including, where applicable, that third parties (including us and other advertisers) may serve content and advertisements, collect information directly from visitors, and place or recognize cookies on visitors’ + browsers; and
    • +
    • + any use that you make of the Content and the JamKazam Marks, whether or not permitted under this Agreement. +
    • +
    +

    + +

    + We will have no liability for these matters or for any of your end users’ claims relating to these matters, and you agree to defend, indemnify, and hold us, our affiliates and licensors, and our and their respective employees, officers, directors, and representatives, harmless from and against all claims, damages, losses, liabilities, costs, and expenses (including attorneys’ + fees) relating to (a) Your Site or any materials that appear on Your Site, including the combination of Your Site or those materials with other applications, content, or processes; (b) the use, development, design, manufacture, production, advertising, promotion, or marketing of Your Site or any materials that appear on or within Your Site, and all other matters described in this Section 5; (c) your use of any Content, whether or not such use is authorized by or violates this Agreement or any applicable law; (d) your violation of any term or condition of this Agreement; or (e) your or your employees' negligence or willful misconduct. +

    + +

    + +

    6. Order Processing

    + +

    + +

    + We will process Product orders placed by Qualifying Customers (defined in Section 7) on the JamKazam Site. We reserve the right to reject orders that do not comply with any requirements on the JamKazam Site, as they may be updated from time to time. We will track Qualifying Purchases (defined in Section 7) for reporting and advertising fee accrual purposes and will make available to you reports summarizing those Qualifying Purchases. +

    + +

    + +

    7. Advertising Fees

    + +

    + +

    + We will pay you advertising fees on Qualifying Purchases in accordance with Section 8 and the Program Advertising Fee Schedule in Section 10. Subject to the exclusions set forth below, a “Qualifying Purchase” + occurs when a Qualifying Customer: (a) purchases a Product within two (2) years of the date on which such Qualifying Customer registered to create his/her JamKazam account; and (b) pays for such Product. A “Qualifying Customer” + is an end user who: (a) clicks through a Special Link on Your Site to the JamKazam Site; and (b) during the single Session created by this click through, registers to create a new JamKazam account. A “Session” + begins when an end user clicks through a Special Link on Your Site to the JamKazam Site and ends when such end user leaves the JamKazam Site. +

    + +

    + +

    + Qualifying Purchases exclude, and we will not pay advertising fees on any of, the following: +

    + +

    +
      +
    • + any Product purchase that is not correctly tracked or reported because the links from Your Site to the JamKazam Site are not properly formatted; +
    • +
    • + any Product purchased through a Special Link by you or on your behalf, including Products you purchase through Special Links for yourself, friends, relatives, or associates; +
    • +
    • + any Product purchased through a Special Link that violates the terms of this Agreement; +
    • +
    • any Product order that is canceled or returned;
    • +
    • any Product purchase that becomes classified as bad debt; or
    • +
    • + any Product purchase for which we process a promotional discount against the transaction that makes such Product free. +
    • +
    +

    + +

    8. Advertising Fee Payment

    + +

    + +

    + We will pay you advertising fees on a quarterly basis for Qualifying Purchases downloaded, shipped, or otherwise fulfilled (as applicable) in a given calendar quarter, subject to any applicable withholding or deduction described below. We will pay you approximately 30 days following the end of each calendar quarter by mailing a check in the amount of the advertising fees you earn to the mailing address then-currently associated with your JamKazam account, but we may accrue and withhold payment of advertising fees until the total amount due to you is at least US$50.00. If you do not have a valid mailing address associated with your JamKazam account within 30 days of the end of a calendar quarter, we will withhold any unpaid accrued advertising fees until you have associated a valid mailing address and notified us that you have done so. +

    + +

    + +

    + Further, any unpaid accrued advertising fees in your account may be subject to escheatment under state law. We may be obligated by law to obtain tax information from you if you are a U.S. citizen, U.S. resident, or U.S. corporation, or if your business is otherwise taxable in the U.S. If we request tax information from you and you do not provide it to us, we may (in addition to any other rights or remedies available to us) withhold your advertising fees until you provide this information or otherwise satisfy us that you are not a person from whom we are required to obtain tax information. +

    + +

    + +

    9. Policies and Pricing

    + +

    + +

    + Qualifying Customers who buy Products through this Program are our customers with respect to all activities they undertake in connection with the JamKazam Site. Accordingly, as between you and us, all pricing, terms of sale, rules, policies, and operating procedures concerning customer orders, customer service, and product sales set forth on the JamKazam Site will apply to such Qualifying Customers, and we may change them at any time in our sole discretion. +

    + +

    + +

    10. Program Advertising Fee Schedule

    + +

    + +

    + We will determine and calculate amounts payable to you as advertising fees for Qualifying Purchases as set forth in the table below (the “Program Advertising Fee Schedule”). +

    + +

    + + + + + + + + + + + + +

    Product

    Advertising Fees

    JamTracks

    + We will pay advertising fees of US$0.20 per JamTrack sold as a Qualifying Purchase. +

    +

    + +

    + From time to time, we may modify this Program Advertising Fee Schedule as part of modifications made to this Agreement. +

    + +

    + +

    11. Limited License

    + +

    + +

    + Subject to the terms of this Agreement and solely for the limited purposes of advertising Products on, and directing end users to, the JamKazam Site in connection with the Program, we hereby grant you a limited, revocable, non-transferable, non-sublicensable, non-exclusive, royalty-free license to (a) copy and display the Content solely on Your Site; and (b) use only those of our trademarks and logos that we may make available to you as part of Content (those trademarks and logos, collectively, “JamKazam Marks”) solely on Your Site. +

    + +

    + +

    + The license set forth in this Section 11 will immediately and automatically terminate if at any time you do not timely comply with any obligation under this Agreement, or otherwise upon termination of this Agreement. In addition, we may terminate the license set forth in this Section 11 in whole or in part upon written notice to you. You will promptly remove from Your Site and delete or otherwise destroy all of the Content and JamKazam Marks with respect to which the license set forth in this Section 11 is terminated or as we may otherwise request from time to time. +

    + +

    + +

    12. Reservation of Rights; Submissions

    + +

    + +

    + Other than the limited licenses expressly set forth in Section 11, we reserve all right, title and interest (including all intellectual property and proprietary rights) in and to, and you do not, by virtue of this Agreement or otherwise, acquire any ownership interest or rights in or to, the Program, Special Links, Content, Products, any domain name owned or operated by us, our trademarks and logos (including the JamKazam Marks), and any other intellectual property and technology that we provide or use in connection with the Program (including any application program interfaces, software development kits, libraries, sample code, and related materials). If you provide us with suggestions, reviews, modifications, data, images, text, or other information or content about a Product or in connection with this Agreement, any Content, or your participation in the Program, or if you modify any Content in any way, (collectively, “Your Submission”), you hereby irrevocably assign to us all right, title, and interest in and to Your Submission and grant us (even if you have designated Your Submission as confidential) a perpetual, paid-up royalty-free, nonexclusive, worldwide, irrevocable, freely transferable right and license to (a) use, reproduce, perform, display, and distribute Your Submission in any manner; (b) adapt, modify, re-format, and create derivative works of Your Submission for any purpose; (c) use and publish your name in the form of a credit in conjunction with Your Submission (however, we will not have any obligation to do so); and (d) sublicense the foregoing rights to any other person or entity. Additionally, you hereby warrant that: (y) Your Submission is your original work, or you obtained Your Submission in a lawful manner; and (z) our and our sublicensees’ + exercise of rights under the license above will not violate any person’s or entity’s rights, including any copyright rights. You agree to provide us such assistance as we may require to document, perfect, or maintain our rights in and to Your Submission. +

    + +

    + +

    13. Compliance with Laws

    + +

    + +

    + In connection with your participation in the Program you will comply with all applicable laws, ordinances, rules, regulations, orders, licenses, permits, judgments, decisions, and other requirements of any governmental authority that has jurisdiction over you, including laws (federal, state, or otherwise) that govern marketing email (e.g., the CAN-SPAM Act of 2003). +

    + +

    + +

    14. Term and Termination

    + +

    + +

    + The term of this Agreement will begin upon your execution of this Agreement by clicking the “I Agree” + button at the end of this Agreement, only if such execution is processed and confirmed successfully by the JamKazam Site, and will end when terminated by either you or us. Either you or we may terminate this Agreement at any time, with or without cause, by giving the other party written notice of termination. Upon any termination of this Agreement, any and all licenses you have with respect to Content will automatically terminate, and you will immediately stop using the Content and JamKazam Marks and promptly remove from Your Site and delete or otherwise destroy all links to the JamKazam Site, all JamKazam Marks, all other Content, and any other materials provided or made available by or on behalf of us to you under this Agreement or otherwise in connection with the Program. We may withhold accrued unpaid advertising fees for a reasonable period of time following termination to ensure that the correct amount is paid (e.g., to account for any cancelations or returns). Upon any termination of this Agreement, all rights and obligations of the parties will be extinguished, except that the rights and obligations of the parties under Sections 5, 9, 12, 13, 14, 16, 17, 18, 19, and 20, together with any of our payment obligations that accrue in accordance with Sections 6, 7, 8, and 10 following the termination of this Agreement, will survive the termination of this Agreement. No termination of this Agreement will relieve either party for any liability for any breach of, or liability accruing under, this Agreement prior to termination. +

    + +

    + +

    15. Modification

    + +

    + +

    + We may modify any of the terms and conditions contained in this Agreement at any time and in our sole discretion by posting a change notice or revised agreement on the JamKazam Site or by sending notice of such modification to you by email to the email address then-currently associated with your JamKazam account (any such change by email will be effective on the date specified in such email and will in no event be less than two business days after the date the email is sent). Modifications may include, for example, changes to the Program Advertising Fee Schedule, payment procedures, and other Program requirements. IF ANY MODIFICATION IS UNACCEPTABLE TO YOU, YOUR ONLY RECOURSE IS TO TERMINATE THIS AGREEMENT. YOUR CONTINUED PARTICIPATION IN THE PROGRAM FOLLOWING THE EFFECTIVE DATE OF ANY MODIFICATION (E.G., THE DATE OF OUR POSTING OF A CHANGE NOTICE OR REVISED AGREEMENT ON THE JAMKAZAM SITE OR THE DATE SPECIFIED IN ANY EMAIL TO YOU REGARDING SUCH MODIFICATION) WILL CONSTITUTE YOUR BINDING ACCEPTANCE OF THE CHANGE. +

    + +

    + +

    16. Relationship of Parties

    + +

    + +

    + You and we are independent contractors, and nothing in this Agreement will create any partnership, joint venture, agency, franchise, sales representative, or employment relationship between you and us or our respective affiliates. You will have no authority to make or accept any offers or representations on our behalf. You will not make any statement, whether on Your Site or otherwise, that contradicts or may contradict anything in this section. If you authorize, assist, encourage, or facilitate another person or entity to take any action related to the subject matter of this Agreement, you will be deemed to have taken the action yourself. +

    + +

    + +

    17. Limitation of Liability

    + +

    + +

    + WE WILL NOT BE LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR EXEMPLARY DAMAGES (INCLUDING ANY LOSS OF REVENUE, PROFITS, GOODWILL, USE, OR DATA) ARISING IN CONNECTION WITH THIS AGREEMENT, THE PROGRAM, THE JAMKAZAM SITE, OR THE SERVICE OFFERINGS (DEFINED BELOW), EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF THOSE DAMAGES. FURTHER, OUR AGGREGATE LIABILITY ARISING IN CONNECTION WITH THIS AGREEMENT, THE PROGRAM, THE JAMKAZAM SITE, AND THE SERVICE OFFERINGS WILL NOT EXCEED THE TOTAL ADVERTISING FEES PAID OR PAYABLE TO YOU UNDER THIS AGREEMENT IN THE TWELVE MONTHS IMMEDIATELY PRECEDING THE DATE ON WHICH THE EVENT GIVING RISE TO THE MOST RECENT CLAIM OF LIABILITY OCCURRED. +

    + +

    + +

    18. Disclaimers

    + +

    + +

    + THE PROGRAM, THE JAMKAZAM SITE, ANY PRODUCTS AND SERVICES OFFERED ON THE JAMKAZAM SITE, ANY SPECIAL LINKS, LINK FORMATS, CONTENT, JAMKAZAM.COM DOMAIN NAME, OUR TRADEMARKS AND LOGOS (INCLUDING THE JAMKAZAM MARKS), AND ALL TECHNOLOGY, SOFTWARE, FUNCTIONS, MATERIALS, DATA, IMAGES, TEXT, AND OTHER INFORMATION AND CONTENT PROVIDED OR USED BY OR ON BEHALF OF US OR LICENSORS IN CONNECTION WITH THE PROGRAM (COLLECTIVELY THE “SERVICE OFFERINGS”) ARE PROVIDED “AS IS.” + NEITHER WE NOR ANY OF OUR LICENSORS MAKE ANY REPRESENTATION OR WARRANTY OF ANY KIND, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE WITH RESPECT TO THE SERVICE OFFERINGS. EXCEPT TO THE EXTENT PROHIBITED BY APPLICABLE LAW, WE AND OUR LICENSORS DISCLAIM ALL WARRANTIES WITH RESPECT TO THE SERVICE OFFERINGS, INCLUDING ANY IMPLIED WARRANTIES OF MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND QUIET ENJOYMENT, AND ANY WARRANTIES ARISING OUT OF ANY COURSE OF DEALING, PERFORMANCE, OR TRADE USAGE. WE MAY DISCONTINUE ANY SERVICE OFFERING, OR MAY CHANGE THE NATURE, FEATURES, FUNCTIONS, SCOPE, OR OPERATION OF ANY SERVICE OFFERING, AT ANY TIME AND FROM TIME TO TIME. NEITHER WE NOR ANY OF OUR LICENSORS WARRANT THAT THE SERVICE OFFERINGS WILL CONTINUE TO BE PROVIDED, WILL FUNCTION AS DESCRIBED, CONSISTENTLY OR IN ANY PARTICULAR MANNER, OR WILL BE UNINTERRUPTED, ACCURATE, ERROR FREE, OR FREE OF HARMFUL COMPONENTS. NEITHER WE NOR ANY OF OUR LICENSORS WILL BE RESPONSIBLE FOR (A) ANY ERRORS, INACCURACIES, OR SERVICE INTERRUPTIONS, INCLUDING POWER OUTAGES OR SYSTEM FAILURES; OR (B) ANY UNAUTHORIZED ACCESS TO OR ALTERATION OF, OR DELETION, DESTRUCTION, DAMAGE, OR LOSS OF, YOUR SITE OR ANY DATA, IMAGES, TEXT, OR OTHER INFORMATION OR CONTENT. NO ADVICE OR INFORMATION OBTAINED BY YOU FROM US OR FROM ANY OTHER PERSON OR ENTITY OR THROUGH THE PROGRAM, CONTENT, OR THE JAMKAZAM SITE WILL CREATE ANY WARRANTY NOT EXPRESSLY STATED IN THIS AGREEMENT. FURTHER, NEITHER WE NOR ANY OF OUR LICENSORS WILL BE RESPONSIBLE FOR ANY COMPENSATION, REIMBURSEMENT, OR DAMAGES ARISING IN CONNECTION WITH (X) ANY LOSS OF PROSPECTIVE PROFITS OR REVENUE, ANTICIPATED SALES, GOODWILL, OR OTHER BENEFITS, (Y) ANY INVESTMENTS, EXPENDITURES, OR COMMITMENTS BY YOU IN CONNECTION WITH THIS AGREEMENT OR YOUR PARTICIPATION IN THE PROGRAM, OR (Z) ANY TERMINATION OF THIS AGREEMENT OR YOUR PARTICIPATION IN THE PROGRAM. +

    + +

    + +

    19. Disputes

    + +

    + +

    + Any dispute relating in any way to the Program or this Agreement will be resolved by binding arbitration, rather than in court, except that you may assert claims in small claims court if your claims qualify. The Federal Arbitration Act and federal arbitration law and the laws of the state of Texas, without regard to principles of conflict of laws, will govern this Agreement and any dispute of any sort that might arise between you and us. +

    + +

    + +

    + There is no judge or jury in arbitration, and court review of an arbitration award is limited. However, an arbitrator can award on an individual basis the same damages and relief as a court (including injunctive and declaratory relief or statutory damages), and must follow the terms of this Agreement as a court would. +

    + +

    + +

    + To begin an arbitration proceeding, you must send a letter requesting arbitration and describing your claim to us at: JamKazam, Inc., Attn: Legal Department, 5813 Lookout Mountain Drive, Austin TX 78731. The arbitration will be conducted by the American Arbitration Association (“AAA”) under its rules, including the AAA’s Supplementary Procedures for Consumer-Related Disputes. The AAA’s rules are available at www.adr.org or by calling 1-800-778-7879. Payment of all filing, administration and arbitrator fees will be governed by the AAA’s rules. We will reimburse those fees for claims totaling less than $10,000 unless the arbitrator determines the claims are frivolous. Likewise, we will not seek attorneys’ + fees and costs in arbitration unless the arbitrator determines the claims are frivolous. You may choose to have the arbitration conducted by telephone, based on written submissions, or in person in the county where you live or at another mutually agreed location. +

    + +

    + +

    + We each agree that any dispute resolution proceedings will be conducted only on an individual basis and not in a class, consolidated or representative action. If for any reason a claim proceeds in court rather than in arbitration, we each waive any right to a jury trial. We also both agree that you or we may bring suit in court to enjoin infringement or other misuse of intellectual property rights. +

    + +

    + +

    + Notwithstanding anything to the contrary in this Agreement, we may seek injunctive or other relief in any state, federal, or national court of competent jurisdiction for any actual or alleged infringement of our or any other person or entity’s intellectual property or proprietary rights. You further acknowledge and agree that our rights in the Content are of a special, unique, extraordinary character, giving them peculiar value, the loss of which cannot be readily estimated or adequately compensated for in monetary damages. +

    + +

    + +

    20. Miscellaneous

    + +

    + +

    + You acknowledge and agree that we may at any time (directly or indirectly) solicit customer referrals on terms that may differ from those contained in this Agreement or operate sites that are similar to or compete with Your Site. You may not assign this Agreement, by operation of law or otherwise, without our express prior written approval. Subject to that restriction, this Agreement will be binding on, inure to the benefit of, and be enforceable against the parties and their respective successors and assigns. Our failure to enforce your strict performance of any provision of this Agreement will not constitute a waiver of our right to subsequently enforce such provision or any other provision of this Agreement. Whenever used in this Agreement, the terms “include(s),” “including,” “e.g.,” + and “for example” mean, respectively, “include(s), without limitation,” “including, without limitation,” “e.g., without limitation,” + and “for example, without limitation.” Any determinations or updates that may be made by us, any actions that may be taken by us, and any approvals that may be given by us under this Agreement, may be made, taken, or given in our sole discretion. +

    + +

    + +

    + +

    + BY CLICKING THE "I AGREE" BUTTON BELOW, YOU AGREE TO THE TERMS AND CONDITIONS OF THIS AGREEMENT, OR BY CONTINUING TO PARTICIPATE IN THE PROGRAM FOLLOWING OUR POSTING OF A CHANGE NOTICE OR REVISED AGREEMENT ON THE JAMKAZAM.COM SITE, YOU (A) AGREE TO BE BOUND BY THIS AGREEMENT; (B) ACKNOWLEDGE AND AGREE THAT YOU HAVE INDEPENDENTLY EVALUATED THE DESIRABILITY OF PARTICIPATING IN THE PROGRAM AND ARE NOT RELYING ON ANY REPRESENTATION, GUARANTEE, OR STATEMENT OTHER THAN AS EXPRESSLY SET FORTH IN THIS AGREEMENT; AND (C) HEREBY REPRESENT AND WARRANT THAT YOU ARE LAWFULLY ABLE TO ENTER INTO CONTRACTS (E.G., YOU ARE NOT A MINOR) AND THAT YOU ARE AND WILL REMAIN IN COMPLIANCE WITH THIS AGREEMENT. IN ADDITION, IF THIS AGREEMENT IS BEING AGREED TO BY A COMPANY OR OTHER LEGAL ENTITY, THEN THE PERSON AGREEING TO THIS AGREEMENT ON BEHALF OF THAT COMPANY OR ENTITY HEREBY REPRESENTS AND WARRANTS THAT HE OR SHE IS AUTHORIZED AND LAWFULLY ABLE TO BIND THAT COMPANY OR ENTITY TO THIS AGREEMENT. +

    + + + +

    + JamKazam Confidential                4/17/2015 +

    +
    \ No newline at end of file diff --git a/web/app/views/legal/partner_agreement_full.html.erb b/web/app/views/legal/partner_agreement_full.html.erb new file mode 100644 index 000000000..1c6203aef --- /dev/null +++ b/web/app/views/legal/partner_agreement_full.html.erb @@ -0,0 +1,5062 @@ + +2015-04 JamKazam Affiliate Agreement v2.docx + + + + +

            

    +

    JamKazam Affiliate Agreement

    + +

    Updated: April 30, 2015.

    + +

    + +

    + This Affiliate Agreement (this “Agreement”) contains the terms and conditions that govern your participation in the JamKazam affiliate marketing program (the “Program”). “JamKazam”, “we,” “us,” + or “our” + means JamKazam, Inc. “You” + or “your” + means the applicant. A “site” + means a website. “JamKazam Site” means the jamkazam.com website or a JamKazam applicaion or any other site that is owned or operated by or on behalf of us and which is identified as participating in the Program in the Program Advertising Fee Schedule in Section 10, as applicable. “Your Site” + means any site(s), software application(s), or content that you create, own, or operate and link to the JamKazam Site. +

    + +

    + +

    1. Description of the Program

    + +

    + +

    + The purpose of the Program is to permit you to advertise Products on Your Site and to earn advertising fees for Qualifying Purchases (defined in Section 7) made by your Qualifying Customers (defined in Section 7). A “Product” + is an item sold on the JamKazam Site and listed in the Program Advertising Fee Schedule in Section 10. In order to facilitate your advertisement of Products, we may make available to you data, images, text, link formats, widgets, links, and other linking tools, and other information in connection with the Program (“Content”). +

    + +

    + +

    2. Enrollment, Suitability, and Communications

    + +

    + +

    To enroll in the Program, you must execute this Agreement by clicking the “I Agree” + button at the end of this Agreement, after having thoroughly reviewed the Agreement.

    + +

    + +

    + Your Site may be considered unsuitable for participation in the Program resulting in termination of this Agreement by JamKazam if Your Site: +

    + +

    +
      +
    • promotes or contains sexually explicit materials;
    • +
    • promotes violence or contains violent materials;
    • +
    • promotes or contains libelous or defamatory materials;
    • +
    • + promotes discrimination, or employs discriminatory practices, based on race, sex, religion, nationality, disability, sexual orientation, or age; +
    • +
    • promotes or undertakes illegal activities;
    • +
    • + is directed toward children under 13 years of age, as defined by the Children’s Online Privacy Protection Act (15 U.S.C. §§ + 6501-6506) and any regulations promulgated thereunder;
    • +
    • + includes any trademark of JamKazam or a variant or misspelling of a trademark of JamKazam in any domain name, subdomain name, or in any username, group name, or other identifier on any social networking site; or +
    • +
    • otherwise violates intellectual property rights.
    • +
    +

    + +

    + You will ensure that the information you provide in executing this Agreement and otherwise associated with your account, including your name, email address, mailing address, tax ID, and other information, is at all times complete, accurate, and up-to-date. We may send notifications (if any), reports (if any), and other communications relating to the Program and this Agreement to the email address then-currently associated with your JamKazam account. You will be deemed to have received all notifications, reports, and other communications sent to that email address, even if the email address associated with your account is no longer current. We may mail checks (if any) payable to you for advertising fees earned under this Agreement to the mailing address then-currently associated with your JamKazam account. You will be deemed to have received all checks sent to that mailing address, even if the mailing address associated with your account is no longer current. If we send a check to a mailing address that is no longer valid, we may, in our sole discretion, choose to issue a stop payment order for such a check and send a new check to a new current address that your provide, but in such a case, we will charge a US$50.00 fee for this exception process. +

    + +

    + +

    + If you are a Non-US person participating in the Program, you agree that you will perform all services under the Agreement outside the United States. +

    + +

    + +

    3. Links on Your Site

    + +

    + +

    + After you have entered into this Agreement, you may display Special Links on Your Site. “Special Links” + are links to the JamKazam Site that you place on Your Site in accordance with this Agreement, that properly utilize the special “tagged” + link formats we specify or provide. Special Links permit accurate tracking, reporting, and accrual of advertising fees. +

    + +

    + +

    + You may earn advertising fees only as described in Section 7 and only with respect to activity on the JamKazam Site occurring directly through Special Links. We will have no obligation to pay you advertising fees if you fail to properly format the links on Your Site to the JamKazam Site as Special Links, including to the extent that such failure may result in any reduction of advertising fee amounts that would otherwise be paid to you under this Agreement. +

    + +

    + +

    4. Program Requirements

    + +

    + +

    You hereby consent to us:

    + +

    +
      +
    • sending you emails relating to the Program from time to time; and
    • +
    • + monitoring, recording, using, and disclosing information about Your Site and visitors to Your Site that we obtain in connection with your display of Special Links in accordance with the JamKazam Privacy Policy. +
    • +
    +

    + +

    5. Responsibility for Your Site

    + +

    + +

    + You will be solely responsible for Your Site including its development, operation, and maintenance, and all materials that appear on or within it. For example, you will be solely responsible for: +

    + +

    +
      +
    • the technical operation of Your Site and all related equipment;
    • +
    • + displaying Special Links and Content on Your Site in compliance with this Agreement and any agreement between you and any other person or entity (including any restrictions or requirements placed on you by any person or entity that hosts Your Site); +
    • +
    • + creating and posting, and ensuring the accuracy, completeness, and appropriateness of, materials posted on Your Site (including all Product descriptions and other Product-related materials and any information you include within or associate with Special Links); +
    • +
    • + using the Content, Your Site, and the materials on or within Your Site in a manner that does not infringe, violate, or misappropriate any of our rights or those of any other person or entity (including copyrights, trademarks, privacy, publicity or other intellectual property or proprietary rights); +
    • +
    • + disclosing on Your Site accurately and adequately, either through a privacy policy or otherwise, how you collect, use, store, and disclose data collected from visitors, including, where applicable, that third parties (including us and other advertisers) may serve content and advertisements, collect information directly from visitors, and place or recognize cookies on visitors’ + browsers; and
    • +
    • + any use that you make of the Content and the JamKazam Marks, whether or not permitted under this Agreement. +
    • +
    +

    + +

    + We will have no liability for these matters or for any of your end users’ claims relating to these matters, and you agree to defend, indemnify, and hold us, our affiliates and licensors, and our and their respective employees, officers, directors, and representatives, harmless from and against all claims, damages, losses, liabilities, costs, and expenses (including attorneys’ + fees) relating to (a) Your Site or any materials that appear on Your Site, including the combination of Your Site or those materials with other applications, content, or processes; (b) the use, development, design, manufacture, production, advertising, promotion, or marketing of Your Site or any materials that appear on or within Your Site, and all other matters described in this Section 5; (c) your use of any Content, whether or not such use is authorized by or violates this Agreement or any applicable law; (d) your violation of any term or condition of this Agreement; or (e) your or your employees' negligence or willful misconduct. +

    + +

    + +

    6. Order Processing

    + +

    + +

    + We will process Product orders placed by Qualifying Customers (defined in Section 7) on the JamKazam Site. We reserve the right to reject orders that do not comply with any requirements on the JamKazam Site, as they may be updated from time to time. We will track Qualifying Purchases (defined in Section 7) for reporting and advertising fee accrual purposes and will make available to you reports summarizing those Qualifying Purchases. +

    + +

    + +

    7. Advertising Fees

    + +

    + +

    + We will pay you advertising fees on Qualifying Purchases in accordance with Section 8 and the Program Advertising Fee Schedule in Section 10. Subject to the exclusions set forth below, a “Qualifying Purchase” + occurs when a Qualifying Customer: (a) purchases a Product within two (2) years of the date on which such Qualifying Customer registered to create his/her JamKazam account; and (b) pays for such Product. A “Qualifying Customer” + is an end user who: (a) clicks through a Special Link on Your Site to the JamKazam Site; and (b) during the single Session created by this click through, registers to create a new JamKazam account. A “Session” + begins when an end user clicks through a Special Link on Your Site to the JamKazam Site and ends when such end user leaves the JamKazam Site. +

    + +

    + +

    + Qualifying Purchases exclude, and we will not pay advertising fees on any of, the following: +

    + +

    +
      +
    • + any Product purchase that is not correctly tracked or reported because the links from Your Site to the JamKazam Site are not properly formatted; +
    • +
    • + any Product purchased through a Special Link by you or on your behalf, including Products you purchase through Special Links for yourself, friends, relatives, or associates; +
    • +
    • + any Product purchased through a Special Link that violates the terms of this Agreement; +
    • +
    • any Product order that is canceled or returned;
    • +
    • any Product purchase that becomes classified as bad debt; or
    • +
    • + any Product purchase for which we process a promotional discount against the transaction that makes such Product free. +
    • +
    +

    + +

    8. Advertising Fee Payment

    + +

    + +

    + We will pay you advertising fees on a quarterly basis for Qualifying Purchases downloaded, shipped, or otherwise fulfilled (as applicable) in a given calendar quarter, subject to any applicable withholding or deduction described below. We will pay you approximately 30 days following the end of each calendar quarter by mailing a check in the amount of the advertising fees you earn to the mailing address then-currently associated with your JamKazam account, but we may accrue and withhold payment of advertising fees until the total amount due to you is at least US$50.00. If you do not have a valid mailing address associated with your JamKazam account within 30 days of the end of a calendar quarter, we will withhold any unpaid accrued advertising fees until you have associated a valid mailing address and notified us that you have done so. +

    + +

    + +

    + Further, any unpaid accrued advertising fees in your account may be subject to escheatment under state law. We may be obligated by law to obtain tax information from you if you are a U.S. citizen, U.S. resident, or U.S. corporation, or if your business is otherwise taxable in the U.S. If we request tax information from you and you do not provide it to us, we may (in addition to any other rights or remedies available to us) withhold your advertising fees until you provide this information or otherwise satisfy us that you are not a person from whom we are required to obtain tax information. +

    + +

    + +

    9. Policies and Pricing

    + +

    + +

    + Qualifying Customers who buy Products through this Program are our customers with respect to all activities they undertake in connection with the JamKazam Site. Accordingly, as between you and us, all pricing, terms of sale, rules, policies, and operating procedures concerning customer orders, customer service, and product sales set forth on the JamKazam Site will apply to such Qualifying Customers, and we may change them at any time in our sole discretion. +

    + +

    + +

    10. Program Advertising Fee Schedule

    + +

    + +

    + We will determine and calculate amounts payable to you as advertising fees for Qualifying Purchases as set forth in the table below (the “Program Advertising Fee Schedule”). +

    + +

    + + + + + + + + + + + + +

    Product

    Advertising Fees

    JamTracks

    + We will pay advertising fees of US$0.20 per JamTrack sold as a Qualifying Purchase. +

    +

    + +

    + From time to time, we may modify this Program Advertising Fee Schedule as part of modifications made to this Agreement. +

    + +

    + +

    11. Limited License

    + +

    + +

    + Subject to the terms of this Agreement and solely for the limited purposes of advertising Products on, and directing end users to, the JamKazam Site in connection with the Program, we hereby grant you a limited, revocable, non-transferable, non-sublicensable, non-exclusive, royalty-free license to (a) copy and display the Content solely on Your Site; and (b) use only those of our trademarks and logos that we may make available to you as part of Content (those trademarks and logos, collectively, “JamKazam Marks”) solely on Your Site. +

    + +

    + +

    + The license set forth in this Section 11 will immediately and automatically terminate if at any time you do not timely comply with any obligation under this Agreement, or otherwise upon termination of this Agreement. In addition, we may terminate the license set forth in this Section 11 in whole or in part upon written notice to you. You will promptly remove from Your Site and delete or otherwise destroy all of the Content and JamKazam Marks with respect to which the license set forth in this Section 11 is terminated or as we may otherwise request from time to time. +

    + +

    + +

    12. Reservation of Rights; Submissions

    + +

    + +

    + Other than the limited licenses expressly set forth in Section 11, we reserve all right, title and interest (including all intellectual property and proprietary rights) in and to, and you do not, by virtue of this Agreement or otherwise, acquire any ownership interest or rights in or to, the Program, Special Links, Content, Products, any domain name owned or operated by us, our trademarks and logos (including the JamKazam Marks), and any other intellectual property and technology that we provide or use in connection with the Program (including any application program interfaces, software development kits, libraries, sample code, and related materials). If you provide us with suggestions, reviews, modifications, data, images, text, or other information or content about a Product or in connection with this Agreement, any Content, or your participation in the Program, or if you modify any Content in any way, (collectively, “Your Submission”), you hereby irrevocably assign to us all right, title, and interest in and to Your Submission and grant us (even if you have designated Your Submission as confidential) a perpetual, paid-up royalty-free, nonexclusive, worldwide, irrevocable, freely transferable right and license to (a) use, reproduce, perform, display, and distribute Your Submission in any manner; (b) adapt, modify, re-format, and create derivative works of Your Submission for any purpose; (c) use and publish your name in the form of a credit in conjunction with Your Submission (however, we will not have any obligation to do so); and (d) sublicense the foregoing rights to any other person or entity. Additionally, you hereby warrant that: (y) Your Submission is your original work, or you obtained Your Submission in a lawful manner; and (z) our and our sublicensees’ + exercise of rights under the license above will not violate any person’s or entity’s rights, including any copyright rights. You agree to provide us such assistance as we may require to document, perfect, or maintain our rights in and to Your Submission. +

    + +

    + +

    13. Compliance with Laws

    + +

    + +

    + In connection with your participation in the Program you will comply with all applicable laws, ordinances, rules, regulations, orders, licenses, permits, judgments, decisions, and other requirements of any governmental authority that has jurisdiction over you, including laws (federal, state, or otherwise) that govern marketing email (e.g., the CAN-SPAM Act of 2003). +

    + +

    + +

    14. Term and Termination

    + +

    + +

    + The term of this Agreement will begin upon your execution of this Agreement by clicking the “I Agree” + button at the end of this Agreement, only if such execution is processed and confirmed successfully by the JamKazam Site, and will end when terminated by either you or us. Either you or we may terminate this Agreement at any time, with or without cause, by giving the other party written notice of termination. Upon any termination of this Agreement, any and all licenses you have with respect to Content will automatically terminate, and you will immediately stop using the Content and JamKazam Marks and promptly remove from Your Site and delete or otherwise destroy all links to the JamKazam Site, all JamKazam Marks, all other Content, and any other materials provided or made available by or on behalf of us to you under this Agreement or otherwise in connection with the Program. We may withhold accrued unpaid advertising fees for a reasonable period of time following termination to ensure that the correct amount is paid (e.g., to account for any cancelations or returns). Upon any termination of this Agreement, all rights and obligations of the parties will be extinguished, except that the rights and obligations of the parties under Sections 5, 9, 12, 13, 14, 16, 17, 18, 19, and 20, together with any of our payment obligations that accrue in accordance with Sections 6, 7, 8, and 10 following the termination of this Agreement, will survive the termination of this Agreement. No termination of this Agreement will relieve either party for any liability for any breach of, or liability accruing under, this Agreement prior to termination. +

    + +

    + +

    15. Modification

    + +

    + +

    + We may modify any of the terms and conditions contained in this Agreement at any time and in our sole discretion by posting a change notice or revised agreement on the JamKazam Site or by sending notice of such modification to you by email to the email address then-currently associated with your JamKazam account (any such change by email will be effective on the date specified in such email and will in no event be less than two business days after the date the email is sent). Modifications may include, for example, changes to the Program Advertising Fee Schedule, payment procedures, and other Program requirements. IF ANY MODIFICATION IS UNACCEPTABLE TO YOU, YOUR ONLY RECOURSE IS TO TERMINATE THIS AGREEMENT. YOUR CONTINUED PARTICIPATION IN THE PROGRAM FOLLOWING THE EFFECTIVE DATE OF ANY MODIFICATION (E.G., THE DATE OF OUR POSTING OF A CHANGE NOTICE OR REVISED AGREEMENT ON THE JAMKAZAM SITE OR THE DATE SPECIFIED IN ANY EMAIL TO YOU REGARDING SUCH MODIFICATION) WILL CONSTITUTE YOUR BINDING ACCEPTANCE OF THE CHANGE. +

    + +

    + +

    16. Relationship of Parties

    + +

    + +

    + You and we are independent contractors, and nothing in this Agreement will create any partnership, joint venture, agency, franchise, sales representative, or employment relationship between you and us or our respective affiliates. You will have no authority to make or accept any offers or representations on our behalf. You will not make any statement, whether on Your Site or otherwise, that contradicts or may contradict anything in this section. If you authorize, assist, encourage, or facilitate another person or entity to take any action related to the subject matter of this Agreement, you will be deemed to have taken the action yourself. +

    + +

    + +

    17. Limitation of Liability

    + +

    + +

    + WE WILL NOT BE LIABLE FOR INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR EXEMPLARY DAMAGES (INCLUDING ANY LOSS OF REVENUE, PROFITS, GOODWILL, USE, OR DATA) ARISING IN CONNECTION WITH THIS AGREEMENT, THE PROGRAM, THE JAMKAZAM SITE, OR THE SERVICE OFFERINGS (DEFINED BELOW), EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF THOSE DAMAGES. FURTHER, OUR AGGREGATE LIABILITY ARISING IN CONNECTION WITH THIS AGREEMENT, THE PROGRAM, THE JAMKAZAM SITE, AND THE SERVICE OFFERINGS WILL NOT EXCEED THE TOTAL ADVERTISING FEES PAID OR PAYABLE TO YOU UNDER THIS AGREEMENT IN THE TWELVE MONTHS IMMEDIATELY PRECEDING THE DATE ON WHICH THE EVENT GIVING RISE TO THE MOST RECENT CLAIM OF LIABILITY OCCURRED. +

    + +

    + +

    18. Disclaimers

    + +

    + +

    + THE PROGRAM, THE JAMKAZAM SITE, ANY PRODUCTS AND SERVICES OFFERED ON THE JAMKAZAM SITE, ANY SPECIAL LINKS, LINK FORMATS, CONTENT, JAMKAZAM.COM DOMAIN NAME, OUR TRADEMARKS AND LOGOS (INCLUDING THE JAMKAZAM MARKS), AND ALL TECHNOLOGY, SOFTWARE, FUNCTIONS, MATERIALS, DATA, IMAGES, TEXT, AND OTHER INFORMATION AND CONTENT PROVIDED OR USED BY OR ON BEHALF OF US OR LICENSORS IN CONNECTION WITH THE PROGRAM (COLLECTIVELY THE “SERVICE OFFERINGS”) ARE PROVIDED “AS IS.” + NEITHER WE NOR ANY OF OUR LICENSORS MAKE ANY REPRESENTATION OR WARRANTY OF ANY KIND, WHETHER EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE WITH RESPECT TO THE SERVICE OFFERINGS. EXCEPT TO THE EXTENT PROHIBITED BY APPLICABLE LAW, WE AND OUR LICENSORS DISCLAIM ALL WARRANTIES WITH RESPECT TO THE SERVICE OFFERINGS, INCLUDING ANY IMPLIED WARRANTIES OF MERCHANTABILITY, SATISFACTORY QUALITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND QUIET ENJOYMENT, AND ANY WARRANTIES ARISING OUT OF ANY COURSE OF DEALING, PERFORMANCE, OR TRADE USAGE. WE MAY DISCONTINUE ANY SERVICE OFFERING, OR MAY CHANGE THE NATURE, FEATURES, FUNCTIONS, SCOPE, OR OPERATION OF ANY SERVICE OFFERING, AT ANY TIME AND FROM TIME TO TIME. NEITHER WE NOR ANY OF OUR LICENSORS WARRANT THAT THE SERVICE OFFERINGS WILL CONTINUE TO BE PROVIDED, WILL FUNCTION AS DESCRIBED, CONSISTENTLY OR IN ANY PARTICULAR MANNER, OR WILL BE UNINTERRUPTED, ACCURATE, ERROR FREE, OR FREE OF HARMFUL COMPONENTS. NEITHER WE NOR ANY OF OUR LICENSORS WILL BE RESPONSIBLE FOR (A) ANY ERRORS, INACCURACIES, OR SERVICE INTERRUPTIONS, INCLUDING POWER OUTAGES OR SYSTEM FAILURES; OR (B) ANY UNAUTHORIZED ACCESS TO OR ALTERATION OF, OR DELETION, DESTRUCTION, DAMAGE, OR LOSS OF, YOUR SITE OR ANY DATA, IMAGES, TEXT, OR OTHER INFORMATION OR CONTENT. NO ADVICE OR INFORMATION OBTAINED BY YOU FROM US OR FROM ANY OTHER PERSON OR ENTITY OR THROUGH THE PROGRAM, CONTENT, OR THE JAMKAZAM SITE WILL CREATE ANY WARRANTY NOT EXPRESSLY STATED IN THIS AGREEMENT. FURTHER, NEITHER WE NOR ANY OF OUR LICENSORS WILL BE RESPONSIBLE FOR ANY COMPENSATION, REIMBURSEMENT, OR DAMAGES ARISING IN CONNECTION WITH (X) ANY LOSS OF PROSPECTIVE PROFITS OR REVENUE, ANTICIPATED SALES, GOODWILL, OR OTHER BENEFITS, (Y) ANY INVESTMENTS, EXPENDITURES, OR COMMITMENTS BY YOU IN CONNECTION WITH THIS AGREEMENT OR YOUR PARTICIPATION IN THE PROGRAM, OR (Z) ANY TERMINATION OF THIS AGREEMENT OR YOUR PARTICIPATION IN THE PROGRAM. +

    + +

    + +

    19. Disputes

    + +

    + +

    + Any dispute relating in any way to the Program or this Agreement will be resolved by binding arbitration, rather than in court, except that you may assert claims in small claims court if your claims qualify. The Federal Arbitration Act and federal arbitration law and the laws of the state of Texas, without regard to principles of conflict of laws, will govern this Agreement and any dispute of any sort that might arise between you and us. +

    + +

    + +

    + There is no judge or jury in arbitration, and court review of an arbitration award is limited. However, an arbitrator can award on an individual basis the same damages and relief as a court (including injunctive and declaratory relief or statutory damages), and must follow the terms of this Agreement as a court would. +

    + +

    + +

    + To begin an arbitration proceeding, you must send a letter requesting arbitration and describing your claim to us at: JamKazam, Inc., Attn: Legal Department, 5813 Lookout Mountain Drive, Austin TX 78731. The arbitration will be conducted by the American Arbitration Association (“AAA”) under its rules, including the AAA’s Supplementary Procedures for Consumer-Related Disputes. The AAA’s rules are available at www.adr.org or by calling 1-800-778-7879. Payment of all filing, administration and arbitrator fees will be governed by the AAA’s rules. We will reimburse those fees for claims totaling less than $10,000 unless the arbitrator determines the claims are frivolous. Likewise, we will not seek attorneys’ + fees and costs in arbitration unless the arbitrator determines the claims are frivolous. You may choose to have the arbitration conducted by telephone, based on written submissions, or in person in the county where you live or at another mutually agreed location. +

    + +

    + +

    + We each agree that any dispute resolution proceedings will be conducted only on an individual basis and not in a class, consolidated or representative action. If for any reason a claim proceeds in court rather than in arbitration, we each waive any right to a jury trial. We also both agree that you or we may bring suit in court to enjoin infringement or other misuse of intellectual property rights. +

    + +

    + +

    + Notwithstanding anything to the contrary in this Agreement, we may seek injunctive or other relief in any state, federal, or national court of competent jurisdiction for any actual or alleged infringement of our or any other person or entity’s intellectual property or proprietary rights. You further acknowledge and agree that our rights in the Content are of a special, unique, extraordinary character, giving them peculiar value, the loss of which cannot be readily estimated or adequately compensated for in monetary damages. +

    + +

    + +

    20. Miscellaneous

    + +

    + +

    + You acknowledge and agree that we may at any time (directly or indirectly) solicit customer referrals on terms that may differ from those contained in this Agreement or operate sites that are similar to or compete with Your Site. You may not assign this Agreement, by operation of law or otherwise, without our express prior written approval. Subject to that restriction, this Agreement will be binding on, inure to the benefit of, and be enforceable against the parties and their respective successors and assigns. Our failure to enforce your strict performance of any provision of this Agreement will not constitute a waiver of our right to subsequently enforce such provision or any other provision of this Agreement. Whenever used in this Agreement, the terms “include(s),” “including,” “e.g.,” + and “for example” mean, respectively, “include(s), without limitation,” “including, without limitation,” “e.g., without limitation,” + and “for example, without limitation.” Any determinations or updates that may be made by us, any actions that may be taken by us, and any approvals that may be given by us under this Agreement, may be made, taken, or given in our sole discretion. +

    + +

    + +

    + +

    + BY CLICKING THE "I AGREE" BUTTON BELOW, YOU AGREE TO THE TERMS AND CONDITIONS OF THIS AGREEMENT, OR BY CONTINUING TO PARTICIPATE IN THE PROGRAM FOLLOWING OUR POSTING OF A CHANGE NOTICE OR REVISED AGREEMENT ON THE JAMKAZAM.COM SITE, YOU (A) AGREE TO BE BOUND BY THIS AGREEMENT; (B) ACKNOWLEDGE AND AGREE THAT YOU HAVE INDEPENDENTLY EVALUATED THE DESIRABILITY OF PARTICIPATING IN THE PROGRAM AND ARE NOT RELYING ON ANY REPRESENTATION, GUARANTEE, OR STATEMENT OTHER THAN AS EXPRESSLY SET FORTH IN THIS AGREEMENT; AND (C) HEREBY REPRESENT AND WARRANT THAT YOU ARE LAWFULLY ABLE TO ENTER INTO CONTRACTS (E.G., YOU ARE NOT A MINOR) AND THAT YOU ARE AND WILL REMAIN IN COMPLIANCE WITH THIS AGREEMENT. IN ADDITION, IF THIS AGREEMENT IS BEING AGREED TO BY A COMPANY OR OTHER LEGAL ENTITY, THEN THE PERSON AGREEING TO THIS AGREEMENT ON BEHALF OF THAT COMPANY OR ENTITY HEREBY REPRESENTS AND WARRANTS THAT HE OR SHE IS AUTHORIZED AND LAWFULLY ABLE TO BIND THAT COMPANY OR ENTITY TO THIS AGREEMENT. +

    + +

    + +

    Radio buttons for:

    +
      +
    • I am entering into this Agreement as an individual
    • +
    • + I am executing this Agreement on behalf of the company or entity listed below
    • +
    +

    + +

    + If the user clicks the entity radio button above, we display an Entity Name label and associated text entry box, and an Entity Type label with a drop down list box with the following options: Sole Proprietorship, Limited Liability Company (LLC), S Corporation, C Corporation, Partnership, Trust/Estate, and Other. +

    + +

    + +

    + Green “I Agree” button on the left / centered. Red “I Do Not Agree” button on the right / centered. +

    + +

    + +

    + +

    + +

    JamKazam Confidential                4/17/2015 +

    + + \ No newline at end of file diff --git a/web/app/views/shared/_facebook_sdk.html.slim b/web/app/views/shared/_facebook_sdk.html.slim index f1ba82ae2..ff6de72df 100644 --- a/web/app/views/shared/_facebook_sdk.html.slim +++ b/web/app/views/shared/_facebook_sdk.html.slim @@ -1 +1 @@ -script src="https://apis.google.com/js/platform.js" async defer \ No newline at end of file +script src="https://apis.google.com/js/platform.js" async="async" defer="defer" \ No newline at end of file diff --git a/web/app/views/shared/_ga.html.erb b/web/app/views/shared/_ga.html.erb index c4fb4d6a1..fd648c627 100644 --- a/web/app/views/shared/_ga.html.erb +++ b/web/app/views/shared/_ga.html.erb @@ -16,9 +16,9 @@ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); <% if Rails.env == "development" %> - ga('create', '<%= Rails.application.config.ga_ua %>', { 'cookieDomain': 'none' }); + ga('create', {'trackingId': '<%= Rails.application.config.ga_ua %>', 'siteSpeedSampleRate': 100, 'cookieDomain': 'none'}); <% else %> - ga('create', '<%= Rails.application.config.ga_ua %>', 'jamkazam.com'); + ga('create', {'trackingId': '<%= Rails.application.config.ga_ua %>', 'siteSpeedSampleRate': 50, 'cookieDomain': 'jamkazam.com'}); <% end %> ga('require', 'displayfeatures'); ga('send', 'pageview', { diff --git a/web/app/views/shared/_google_nocaptcha.html.slim b/web/app/views/shared/_google_nocaptcha.html.slim index c0df3c331..c804c37b7 100644 --- a/web/app/views/shared/_google_nocaptcha.html.slim +++ b/web/app/views/shared/_google_nocaptcha.html.slim @@ -1 +1 @@ -script src="https://www.google.com/recaptcha/api.js" async defer \ No newline at end of file +script src="https://www.google.com/recaptcha/api.js" async="async" defer="defer" \ No newline at end of file diff --git a/web/app/views/shared/_recurly.html.slim b/web/app/views/shared/_recurly.html.slim index d22bdabbd..7f566e136 100644 --- a/web/app/views/shared/_recurly.html.slim +++ b/web/app/views/shared/_recurly.html.slim @@ -1,4 +1 @@ -script src="https://js.recurly.com/v3/recurly.js" - -javascript: - recurly.configure(gon.global.recurly_public_api_key) \ No newline at end of file +script src="https://js.recurly.com/v3/recurly.js" async="async" defer="defer" \ No newline at end of file diff --git a/web/app/views/users/new.html.erb b/web/app/views/users/new.html.erb index f7b718af5..01fc14c25 100644 --- a/web/app/views/users/new.html.erb +++ b/web/app/views/users/new.html.erb @@ -145,7 +145,7 @@
    - +
    <% end %> diff --git a/web/config/application.rb b/web/config/application.rb index 4de4e6d8c..1e61dc1ab 100644 --- a/web/config/application.rb +++ b/web/config/application.rb @@ -341,7 +341,12 @@ if defined?(Bundler) config.remove_whitespace_credit_card = false config.estimate_taxes = true config.ad_sense_enabled = false - config.guard_against_fraud = false + config.guard_against_fraud = true config.error_on_fraud = false + config.expire_fingerprint_days = 14 + config.found_conflict_count = 1 + config.web_performance_timing_enabled = true + config.jamtrack_landing_bubbles_enabled = true + config.jamtrack_browser_bubbles_enabled = true end end diff --git a/web/config/environments/test.rb b/web/config/environments/test.rb index 920391264..a5b075118 100644 --- a/web/config/environments/test.rb +++ b/web/config/environments/test.rb @@ -108,5 +108,6 @@ SampleApp::Application.configure do config.jam_tracks_available = true config.video_available = "full" config.guard_against_fraud = true + config.error_on_fraud = false end diff --git a/web/config/initializers/gon.rb b/web/config/initializers/gon.rb index 2201a6833..587a89626 100644 --- a/web/config/initializers/gon.rb +++ b/web/config/initializers/gon.rb @@ -16,4 +16,8 @@ Gon.global.video_available = Rails.application.config.video_available Gon.global.gear_check_ignore_high_latency = Rails.application.config.gear_check_ignore_high_latency Gon.global.purchases_enabled = Rails.application.config.purchases_enabled Gon.global.estimate_taxes = Rails.application.config.estimate_taxes +Gon.global.web_performance_timing_enabled = Rails.application.config.web_performance_timing_enabled +Gon.global.jamtrack_landing_bubbles_enabled = Rails.application.config.jamtrack_landing_bubbles_enabled +Gon.global.jamtrack_browser_bubbles_enabled = Rails.application.config.jamtrack_browser_bubbles_enabled +Gon.global.bugsnag_key = Rails.application.config.bugsnag_key Gon.global.env = Rails.env diff --git a/web/config/routes.rb b/web/config/routes.rb index 1e2f26f6f..baa49ee61 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -32,6 +32,9 @@ SampleApp::Application.routes.draw do match '/landing/kick4', to: 'landings#watch_overview_kick4', via: :get, as: 'landing_kick4' match '/landing/jamtracks/:plan_code', to: 'landings#individual_jamtrack', via: :get, as: 'individual_jamtrack' match '/landing/jamtracks/band/:plan_code', to: 'landings#individual_jamtrack_band', via: :get, as: 'individual_jamtrack_band' + match '/affiliateProgram', to: 'landings#affiliate_program', via: :get, as: 'affiliate_program' + + match '/affiliate/links/:id', to: 'affiliates#links', via: :get, as: 'affilate_links' # redirect /jamtracks to jamtracks browse page get '/jamtracks', to: redirect('/client#/jamtrackBrowse') @@ -389,6 +392,13 @@ SampleApp::Application.routes.draw do match '/users/:id/plays' => 'api_users#add_play', :via => :post, :as => 'api_users_add_play' match '/users/:id/affiliate' => 'api_users#affiliate_report', :via => :get, :as => 'api_users_affiliate' + match '/users/:id/affiliate_partner' => 'api_users#affiliate_partner', :via => [:get, :post], :as => 'api_users_affiliate_partner' + + match '/affiliate_partners' => 'api_affiliate#create', :via => :post, :as => 'api_affiliates_create' + match '/affiliate_partners/signups' => 'api_affiliate#traffic_index', :via => :get, :as => 'api_affiliates_signups' + match '/affiliate_partners/monthly_earnings' => 'api_affiliate#monthly_index', :via => :get, :as => 'api_affiliates_monthly' + match '/affiliate_partners/quarterly_earnings' => 'api_affiliate#quarterly_index', :via => :get, :as => 'api_affiliates_quarterly' + match '/affiliate_partners/payments' => 'api_affiliate#payment_index', :via => :get, :as => 'api_affiliates_payment' # downloads/uploads match '/users/:id/syncs' => 'api_user_syncs#index', :via => :get @@ -575,5 +585,13 @@ SampleApp::Application.routes.draw do match '/signup_hints/:id' => 'api_signup_hints#show', :via => :get, :as => :api_signup_hint_detail match '/alerts' => 'api_alerts#create', :via => :post + + # links generated to help affiliates share relevant links + match '/links/jamtrack_songs' => 'api_links#jamtrack_song_index' + match '/links/jamtrack_bands' => 'api_links#jamtrack_band_index' + match '/links/jamtrack_general' => 'api_links#jamtrack_general_index' + match '/links/jamkazam' => 'api_links#jamkazam_general_index' + match '/links/sessions' => 'api_links#session_index' + match '/links/recordings' => 'api_links#recording_index' end end diff --git a/web/config/scheduler.yml b/web/config/scheduler.yml index fb899f1bf..1890a41d4 100644 --- a/web/config/scheduler.yml +++ b/web/config/scheduler.yml @@ -74,3 +74,8 @@ StatsMaker: cron: "* * * * *" class: "JamRuby::StatsMaker" description: "Generates interesting stats from the database" + +TallyAffiliates: + cron: "0 0,4,8,12,16,20 * * *" + class: "JamRuby::TallyAffiliates" + description: "Tallies up affiliate totals" diff --git a/web/db/schema.rb b/web/db/schema.rb new file mode 100644 index 000000000..287f828b8 --- /dev/null +++ b/web/db/schema.rb @@ -0,0 +1,1752 @@ +# encoding: UTF-8 +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended to check this file into your version control system. + +ActiveRecord::Schema.define(:version => 0) do + + add_extension "fuzzystrmatch" + add_extension "postgis" + add_extension "postgis_tiger_geocoder" + add_extension "postgis_topology" + add_extension "tablefunc" + add_extension "uuid-ossp" + + create_table "active_admin_comments", :force => true do |t| + t.string "resource_id", :null => false + t.string "resource_type", :null => false + t.integer "author_id" + t.string "author_type" + t.text "body" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "namespace" + end + + add_index "active_admin_comments", ["author_type", "author_id"], :name => "index_active_admin_comments_on_author_type_and_author_id" + add_index "active_admin_comments", ["namespace"], :name => "index_active_admin_comments_on_namespace" + add_index "active_admin_comments", ["resource_type", "resource_id"], :name => "index_admin_comments_on_resource_type_and_resource_id" + + create_table "active_music_sessions", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "claimed_recording_id", :limit => 64 + t.string "claimed_recording_initiator_id", :limit => 64 + t.integer "track_changes_counter", :default => 0 + t.integer "jam_track_id", :limit => 8 + t.string "jam_track_initiator_id", :limit => 64 + t.string "backing_track_path", :limit => 1024 + t.string "backing_track_initiator_id", :limit => 64 + t.boolean "metronome_active", :default => false, :null => false + t.string "metronome_initiator_id", :limit => 64 + end + + create_table "addr", :primary_key => "gid", :force => true do |t| + t.integer "tlid", :limit => 8 + t.string "fromhn", :limit => 12 + t.string "tohn", :limit => 12 + t.string "side", :limit => 1 + t.string "zip", :limit => 5 + t.string "plus4", :limit => 4 + t.string "fromtyp", :limit => 1 + t.string "totyp", :limit => 1 + t.integer "fromarmid" + t.integer "toarmid" + t.string "arid", :limit => 22 + t.string "mtfcc", :limit => 5 + t.string "statefp", :limit => 2 + end + + add_index "addr", ["tlid", "statefp"], :name => "idx_tiger_addr_tlid_statefp" + add_index "addr", ["zip"], :name => "idx_tiger_addr_zip" + +# Could not dump table "addrfeat" because of following StandardError +# Unknown type 'geometry' for column 'the_geom' + + create_table "affiliate_partners", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "partner_name", :limit => 128, :null => false + t.string "partner_code", :limit => 128, :null => false + t.string "partner_user_id", :limit => 64, :null => false + t.string "user_email" + t.integer "referral_user_count", :default => 0, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "affiliate_partners", ["partner_code"], :name => "affiliate_partners_code_idx" + add_index "affiliate_partners", ["partner_user_id"], :name => "affiliate_partners_user_idx" + + create_table "artifact_updates", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "product", :null => false + t.string "version", :null => false + t.string "uri", :limit => 2000, :null => false + t.string "sha1", :null => false + t.string "environment", :default => "public", :null => false + t.integer "size", :null => false + end + + add_index "artifact_updates", ["product", "version"], :name => "artifact_updates_uniqkey", :unique => true + + create_table "backing_tracks", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "filename", :limit => 1024, :null => false + t.string "connection_id", :limit => 64, :null => false + t.string "client_track_id", :limit => 64, :null => false + t.string "client_resource_id", :limit => 100 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "band_invitations", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64 + t.string "band_id", :limit => 64 + t.boolean "accepted" + t.string "creator_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "bands", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "name", :limit => 1024, :null => false + t.string "website", :limit => 4000 + t.string "biography", :limit => 4000, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "city", :limit => 100 + t.string "state", :limit => 100 + t.string "country", :limit => 100 + t.string "photo_url", :limit => 2048 + t.string "logo_url", :limit => 2048 + t.tsvector "name_tsv" + t.string "original_fpfile_photo", :limit => 8000 + t.string "cropped_fpfile_photo", :limit => 8000 + t.string "cropped_s3_path_photo", :limit => 512 + t.string "crop_selection_photo", :limit => 256 + t.decimal "lat", :precision => 15, :scale => 10 + t.decimal "lng", :precision => 15, :scale => 10 + t.string "large_photo_url", :limit => 2048 + t.string "cropped_large_s3_path_photo", :limit => 512 + t.string "cropped_large_fpfile_photo", :limit => 8000 + t.boolean "did_real_session", :default => false + end + + add_index "bands", ["name_tsv"], :name => "bands_name_tsv_index", :using => :gin + + create_table "bands_musicians", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "band_id", :limit => 64, :null => false + t.string "user_id", :limit => 64, :null => false + t.boolean "admin", :default => false, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "bands_musicians", ["band_id", "user_id"], :name => "band_musician_uniqkey", :unique => true + +# Could not dump table "bg" because of following StandardError +# Unknown type 'geometry' for column 'the_geom' + + create_table "chat_messages", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64 + t.string "music_session_id", :limit => 64 + t.text "message", :null => false + t.datetime "created_at", :null => false + end + + create_table "cities", :id => false, :force => true do |t| + t.string "city", :null => false + t.string "region", :limit => 2, :null => false + t.string "countrycode", :limit => 2, :null => false + end + + create_table "claimed_recordings", :id => false, :force => true do |t| + t.string "user_id", :limit => 64, :null => false + t.string "recording_id", :limit => 64, :null => false + t.string "id", :limit => 64, :null => false + t.string "name", :limit => 200, :null => false + t.boolean "is_public", :default => true, :null => false + t.string "genre_id", :limit => 64, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "description", :limit => 8000 + t.tsvector "description_tsv" + t.tsvector "name_tsv" + t.boolean "discarded", :default => false + t.boolean "upload_to_youtube", :default => false, :null => false + end + + add_index "claimed_recordings", ["description_tsv"], :name => "claimed_recordings_description_tsv_index", :using => :gin + add_index "claimed_recordings", ["name_tsv"], :name => "claimed_recordings_name_tsv_index", :using => :gin + add_index "claimed_recordings", ["user_id", "recording_id"], :name => "musician_recording_uniqkey", :unique => true + +# Could not dump table "cohorts" because of following StandardError +# Unknown type 'json' for column 'data_set' + + create_table "connections", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64 + t.string "client_id", :limit => 64, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "music_session_id", :limit => 64 + t.string "ip_address", :limit => 64, :null => false + t.boolean "as_musician" + t.string "aasm_state", :limit => 64, :default => "idle", :null => false + t.integer "addr", :limit => 8, :null => false + t.integer "locidispid", :limit => 8 + t.datetime "joined_session_at" + t.string "client_type", :limit => 256, :null => false + t.integer "stale_time", :default => 40, :null => false + t.integer "expire_time", :default => 60, :null => false + t.float "last_jam_audio_latency" + t.string "channel_id", :limit => 256, :null => false + t.boolean "udp_reachable", :default => true, :null => false + t.datetime "scoring_timeout", :null => false + t.integer "scoring_failures", :default => 0, :null => false + t.integer "scoring_timeout_occurrences", :default => 0, :null => false + t.integer "scoring_failures_offset", :default => 0, :null => false + t.string "gateway", :limit => nil, :default => "default-1", :null => false + t.boolean "is_network_testing", :default => false, :null => false + end + + add_index "connections", ["client_id"], :name => "connections_client_id_key", :unique => true + add_index "connections", ["locidispid"], :name => "connections_locidispid_ndx" + + create_table "countries", :id => false, :force => true do |t| + t.string "countrycode", :limit => 2, :null => false + t.string "countryname", :limit => 64 + end + +# Could not dump table "county" because of following StandardError +# Unknown type 'geometry' for column 'the_geom' + + create_table "county_lookup", :id => false, :force => true do |t| + t.integer "st_code", :null => false + t.string "state", :limit => 2 + t.integer "co_code", :null => false + t.string "name", :limit => 90 + end + + add_index "county_lookup", ["state"], :name => "county_lookup_state_idx" + + create_table "countysub_lookup", :id => false, :force => true do |t| + t.integer "st_code", :null => false + t.string "state", :limit => 2 + t.integer "co_code", :null => false + t.string "county", :limit => 90 + t.integer "cs_code", :null => false + t.string "name", :limit => 90 + end + + add_index "countysub_lookup", ["state"], :name => "countysub_lookup_state_idx" + +# Could not dump table "cousub" because of following StandardError +# Unknown type 'geometry' for column 'the_geom' + + create_table "crash_dumps", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "client_type", :limit => 64, :null => false + t.string "client_id", :limit => 64 + t.string "user_id", :limit => 64 + t.string "session_id", :limit => 64 + t.datetime "timestamp" + t.string "uri", :limit => 1000 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "client_version", :limit => 100, :null => false + end + + add_index "crash_dumps", ["client_id"], :name => "crash_dumps_client_id_idx" + add_index "crash_dumps", ["timestamp"], :name => "crash_dumps_timestamp_idx" + add_index "crash_dumps", ["user_id"], :name => "crash_dumps_user_id_idx" + + create_table "current_network_scores", :id => false, :force => true do |t| + t.integer "alocidispid", :limit => 8, :null => false + t.integer "blocidispid", :limit => 8, :null => false + t.integer "score", :null => false + t.boolean "limited", :null => false + t.datetime "score_dt", :null => false + end + + add_index "current_network_scores", ["alocidispid", "blocidispid"], :name => "current_network_scores_a_b_ndx", :unique => true + add_index "current_network_scores", ["blocidispid", "alocidispid"], :name => "current_network_scores_b_a_ndx", :unique => true + + create_table "diagnostics", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64, :null => false + t.string "type", :null => false + t.string "creator", :null => false + t.text "data" + t.datetime "created_at", :null => false + end + + add_index "diagnostics", ["type"], :name => "diagnostics_type_idx" + add_index "diagnostics", ["user_id"], :name => "diagnostics_user_id" + + create_table "direction_lookup", :id => false, :force => true do |t| + t.string "name", :limit => 20, :null => false + t.string "abbrev", :limit => 3 + end + + add_index "direction_lookup", ["abbrev"], :name => "direction_lookup_abbrev_idx" + +# Could not dump table "edges" because of following StandardError +# Unknown type 'geometry' for column 'the_geom' + + create_table "email_batch_sets", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "email_batch_id", :limit => 64 + t.datetime "started_at" + t.text "user_ids", :default => "", :null => false + t.integer "batch_count" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.integer "trigger_index", :default => 0, :null => false + t.string "sub_type", :limit => 64 + t.string "user_id", :limit => 64 + end + + add_index "email_batch_sets", ["email_batch_id", "started_at"], :name => "email_batch_set_uniqkey", :unique => true + add_index "email_batch_sets", ["email_batch_id"], :name => "email_batch_set_fkidx" + add_index "email_batch_sets", ["user_id", "sub_type"], :name => "email_batch_sets_progress_idx" + + create_table "email_batches", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "subject", :limit => 256 + t.text "body" + t.string "from_email", :limit => 64, :default => "JamKazam ", :null => false + t.string "aasm_state", :limit => 32, :default => "pending", :null => false + t.text "test_emails", :default => "test@jamkazam.com", :null => false + t.integer "opt_in_count", :default => 0, :null => false + t.integer "sent_count", :default => 0, :null => false + t.integer "lock_version" + t.datetime "started_at" + t.datetime "completed_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "type", :limit => 64, :default => "JamRuby::EmailBatch", :null => false + t.string "sub_type", :limit => 64 + end + + create_table "email_errors", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64 + t.string "error_type", :limit => 32 + t.string "email_address", :limit => 256 + t.string "status", :limit => 32 + t.datetime "email_date" + t.text "reason" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "email_errors", ["email_address"], :name => "email_error_address_idx" + add_index "email_errors", ["user_id"], :name => "email_error_user_fkidx" + + create_table "event_sessions", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.datetime "starts_at" + t.datetime "ends_at" + t.string "pinned_state" + t.string "img_url", :limit => 1024 + t.integer "img_width" + t.integer "img_height" + t.string "event_id", :limit => 64 + t.string "user_id", :limit => 64 + t.string "band_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.integer "ordinal" + end + + create_table "events", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "slug", :limit => 512, :null => false + t.text "title" + t.text "description" + t.boolean "show_sponser", :default => false, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.text "social_description" + end + + add_index "events", ["slug"], :name => "events_slug_key", :unique => true + + create_table "facebook_signups", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "lookup_id", :null => false + t.string "last_name", :limit => 100 + t.string "first_name", :limit => 100 + t.string "gender", :limit => 1 + t.string "email", :limit => 1024 + t.string "uid", :limit => 1024 + t.string "token", :limit => 1024 + t.datetime "token_expires_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "facebook_signups", ["lookup_id"], :name => "facebook_signups_lookup_id_key", :unique => true + +# Could not dump table "faces" because of following StandardError +# Unknown type 'geometry' for column 'the_geom' + + create_table "fan_invitations", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "sender_id", :limit => 64 + t.string "receiver_id", :limit => 64 + t.string "music_session_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "featnames", :primary_key => "gid", :force => true do |t| + t.integer "tlid", :limit => 8 + t.string "fullname", :limit => 100 + t.string "name", :limit => 100 + t.string "predirabrv", :limit => 15 + t.string "pretypabrv", :limit => 50 + t.string "prequalabr", :limit => 15 + t.string "sufdirabrv", :limit => 15 + t.string "suftypabrv", :limit => 50 + t.string "sufqualabr", :limit => 15 + t.string "predir", :limit => 2 + t.string "pretyp", :limit => 3 + t.string "prequal", :limit => 2 + t.string "sufdir", :limit => 2 + t.string "suftyp", :limit => 3 + t.string "sufqual", :limit => 2 + t.string "linearid", :limit => 22 + t.string "mtfcc", :limit => 5 + t.string "paflag", :limit => 1 + t.string "statefp", :limit => 2 + end + + add_index "featnames", ["tlid", "statefp"], :name => "idx_tiger_featnames_tlid_statefp" + + create_table "feeds", :force => true do |t| + t.string "recording_id", :limit => 64 + t.string "music_session_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.boolean "active", :default => false + end + + add_index "feeds", ["music_session_id"], :name => "feeds_music_session_id_key", :unique => true + add_index "feeds", ["recording_id"], :name => "feeds_recording_id_key", :unique => true + + create_table "follows", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64, :null => false + t.string "followable_id", :limit => 64, :null => false + t.string "followable_type", :limit => 25, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "follows", ["user_id", "followable_id"], :name => "follows_user_uniqkey", :unique => true + + create_table "friend_requests", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64 + t.string "friend_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "status", :limit => 50 + t.string "message", :limit => 4000 + end + + create_table "friendships", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64 + t.string "friend_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "friendships", ["user_id", "friend_id"], :name => "user_friend_uniqkey", :unique => true + + create_table "generic_state", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.datetime "score_history_last_imported_at" + t.string "env", :default => "development", :null => false + end + + create_table "genre_players", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "player_id", :limit => 64, :null => false + t.string "genre_id", :limit => 64, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "player_type", :limit => 128 + end + + add_index "genre_players", ["player_id", "player_type", "genre_id"], :name => "genre_player_uniqkey", :unique => true + + create_table "genres", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "description", :limit => 1024, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "genres_music_sessions", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "genre_id", :limit => 64 + t.string "music_session_id", :limit => 64 + end + + create_table "geocode_settings", :id => false, :force => true do |t| + t.text "name", :null => false + t.text "setting" + t.text "unit" + t.text "category" + t.text "short_desc" + end + +# Could not dump table "geoipblocks" because of following StandardError +# Unknown type 'geometry(Polygon)' for column 'geom' + + create_table "geoipisp", :id => false, :force => true do |t| + t.integer "beginip", :limit => 8, :null => false + t.integer "endip", :limit => 8, :null => false + t.string "company", :limit => 50, :null => false + end + + add_index "geoipisp", ["company"], :name => "geoipisp_company_ndx" + + create_table "geoiplocations", :id => false, :force => true do |t| + t.integer "locid", :null => false + t.string "countrycode", :limit => 2 + t.string "region", :limit => 2 + t.string "city" + t.string "postalcode", :limit => 8 + t.float "latitude", :null => false + t.float "longitude", :null => false + t.integer "metrocode" + t.string "areacode", :limit => 3 + t.integer "geog", :limit => 0 + end + + add_index "geoiplocations", ["geog"], :name => "geoiplocations_geog_gix", :using => :gist + + create_table "icecast_admin_authentications", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "source_pass", :limit => 64, :null => false + t.string "relay_user", :limit => 64, :null => false + t.string "relay_pass", :limit => 64, :null => false + t.string "admin_user", :limit => 64, :null => false + t.string "admin_pass", :limit => 64, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "icecast_directories", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.integer "yp_url_timeout", :default => 15, :null => false + t.string "yp_url", :limit => 1024, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "icecast_limits", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.integer "clients", :default => 1000, :null => false + t.integer "sources", :default => 50, :null => false + t.integer "queue_size", :default => 102400, :null => false + t.integer "client_timeout", :default => 30 + t.integer "header_timeout", :default => 15 + t.integer "source_timeout", :default => 10 + t.integer "burst_size", :default => 65536 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "icecast_listen_sockets", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.integer "port", :default => 8001, :null => false + t.string "bind_address", :limit => 1024 + t.string "shoutcast_mount", :limit => 1024 + t.integer "shoutcast_compat" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "icecast_loggings", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "access_log", :limit => 1024, :default => "access.log", :null => false + t.string "error_log", :limit => 1024, :default => "error.log", :null => false + t.string "playlist_log", :limit => 1024 + t.integer "log_level", :default => 3, :null => false + t.integer "log_archive" + t.integer "log_size", :default => 10000 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "icecast_master_server_relays", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "master_server", :limit => 1024, :null => false + t.integer "master_server_port", :default => 8001, :null => false + t.integer "master_update_interval", :default => 120, :null => false + t.string "master_username", :limit => 64, :null => false + t.string "master_pass", :limit => 64, :null => false + t.integer "relays_on_demand", :default => 1, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "icecast_mount_templates", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "name", :limit => 256, :null => false + t.string "source_username", :limit => 64 + t.string "source_pass", :limit => 64 + t.integer "max_listeners", :default => 10000 + t.integer "max_listener_duration", :default => 3600 + t.string "dump_file", :limit => 1024 + t.string "intro", :limit => 1024 + t.string "fallback_mount", :limit => 1024 + t.integer "fallback_override", :default => 1 + t.integer "fallback_when_full", :default => 1 + t.string "charset", :limit => 1024, :default => "ISO8859-1" + t.integer "is_public", :default => 0 + t.string "stream_name", :limit => 1024 + t.string "stream_description", :limit => 10000 + t.string "stream_url", :limit => 1024 + t.string "genre", :limit => 256 + t.integer "bitrate" + t.string "mime_type", :limit => 64, :default => "audio/mpeg", :null => false + t.string "subtype", :limit => 64 + t.integer "burst_size" + t.integer "mp3_metadata_interval" + t.integer "hidden", :default => 1 + t.string "on_connect", :limit => 1024 + t.string "on_disconnect", :limit => 1024 + t.string "authentication_id", :limit => 64, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "icecast_mounts", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "name", :limit => 1024, :null => false + t.string "source_username", :limit => 64 + t.string "source_pass", :limit => 64 + t.integer "max_listeners", :default => 10000 + t.integer "max_listener_duration", :default => 3600 + t.string "dump_file", :limit => 1024 + t.string "intro", :limit => 1024 + t.string "fallback_mount", :limit => 1024 + t.integer "fallback_override", :default => 1 + t.integer "fallback_when_full", :default => 1 + t.string "charset", :limit => 1024, :default => "ISO8859-1" + t.integer "is_public", :default => 0 + t.string "stream_name", :limit => 1024 + t.string "stream_description", :limit => 10000 + t.string "stream_url", :limit => 1024 + t.string "genre", :limit => 256 + t.integer "bitrate" + t.string "mime_type", :limit => 64 + t.string "subtype", :limit => 64 + t.integer "burst_size" + t.integer "mp3_metadata_interval" + t.integer "hidden", :default => 1 + t.string "on_connect", :limit => 1024 + t.string "on_disconnect", :limit => 1024 + t.string "authentication_id", :limit => 64 + t.integer "listeners", :default => 0, :null => false + t.boolean "sourced", :default => false, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "music_session_id", :limit => 64 + t.string "icecast_server_id", :limit => 64, :null => false + t.string "icecast_mount_template_id", :limit => 64 + t.datetime "sourced_needs_changing_at" + t.boolean "source_direction", :default => false, :null => false + end + + add_index "icecast_mounts", ["name"], :name => "icecast_mounts_name_key", :unique => true + + create_table "icecast_paths", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "base_dir", :limit => 1024, :default => "./", :null => false + t.string "log_dir", :limit => 1024, :default => "./logs", :null => false + t.string "pid_file", :limit => 1024, :default => "./icecast.pid" + t.string "web_root", :limit => 1024, :default => "./web", :null => false + t.string "admin_root", :limit => 1024, :default => "./admin", :null => false + t.string "allow_ip", :limit => 1024 + t.string "deny_ip", :limit => 1024 + t.string "alias_source", :limit => 1024 + t.string "alias_dest", :limit => 1024 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "icecast_relays", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "server", :limit => 1024, :null => false + t.integer "port", :default => 8001, :null => false + t.string "mount", :limit => 1024, :null => false + t.string "local_mount", :limit => 1024 + t.string "relay_username", :limit => 64 + t.string "relay_pass", :limit => 64 + t.integer "relay_shoutcast_metadata", :default => 0 + t.integer "on_demand", :default => 1 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "icecast_securities", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.integer "chroot", :default => 0, :null => false + t.string "change_owner_user", :limit => 64 + t.string "change_owner_group", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "icecast_server_groups", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "name", :null => false + end + + add_index "icecast_server_groups", ["name"], :name => "icecast_server_groups_name_key", :unique => true + + create_table "icecast_server_mounts", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "icecast_mount_id", :limit => 64 + t.string "icecast_server_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "icecast_server_mounts", ["icecast_mount_id", "icecast_server_id"], :name => "server_mount_uniqkey", :unique => true + + create_table "icecast_server_relays", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "icecast_relay_id", :limit => 64 + t.string "icecast_server_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "icecast_server_relays", ["icecast_relay_id", "icecast_server_id"], :name => "server_relay_uniqkey", :unique => true + + create_table "icecast_server_sockets", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "icecast_listen_socket_id", :limit => 64 + t.string "icecast_server_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "icecast_server_sockets", ["icecast_listen_socket_id", "icecast_server_id"], :name => "server_socket_uniqkey", :unique => true + + create_table "icecast_servers", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.integer "config_changed", :default => 0 + t.string "limit_id", :limit => 64 + t.string "admin_auth_id", :limit => 64 + t.string "directory_id", :limit => 64 + t.string "master_relay_id", :limit => 64 + t.string "path_id", :limit => 64 + t.string "logging_id", :limit => 64 + t.string "security_id", :limit => 64 + t.string "template_id", :limit => 64, :null => false + t.string "hostname", :limit => 1024, :null => false + t.string "server_id", :limit => 1024, :null => false + t.string "location", :limit => 1024 + t.string "admin_email", :limit => 1024 + t.integer "fileserve" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "icecast_server_group_id", :limit => 64, :default => "default", :null => false + t.string "mount_template_id", :limit => 64 + t.datetime "config_updated_at" + end + + add_index "icecast_servers", ["server_id"], :name => "icecast_servers_server_id_key", :unique => true + + create_table "icecast_source_changes", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.boolean "source_direction", :null => false + t.string "change_type", :limit => 64, :null => false + t.string "user_id", :limit => 64 + t.string "client_id", :limit => 64 + t.boolean "success", :null => false + t.string "reason", :limit => nil + t.string "detail", :limit => nil + t.datetime "created_at", :null => false + t.string "icecast_mount_id", :limit => 64, :null => false + end + + create_table "icecast_template_sockets", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "icecast_listen_socket_id", :limit => 64 + t.string "icecast_template_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "icecast_template_sockets", ["icecast_listen_socket_id", "icecast_template_id"], :name => "template_socket_uniqkey", :unique => true + + create_table "icecast_templates", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "limit_id", :limit => 64 + t.string "admin_auth_id", :limit => 64 + t.string "directory_id", :limit => 64 + t.string "master_relay_id", :limit => 64 + t.string "path_id", :limit => 64 + t.string "logging_id", :limit => 64 + t.string "security_id", :limit => 64 + t.string "location", :limit => 1024, :null => false + t.string "name", :limit => 256, :null => false + t.string "admin_email", :limit => 1024, :default => "admin@jamkazam.com", :null => false + t.integer "fileserve", :default => 1, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "icecast_user_authentications", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "authentication_type", :limit => 16, :default => "url" + t.string "filename", :limit => 1024 + t.integer "allow_duplicate_users" + t.string "mount_add", :limit => 1024 + t.string "mount_remove", :limit => 1024 + t.string "listener_add", :limit => 1024 + t.string "listener_remove", :limit => 1024 + t.string "unused_username", :limit => 64 + t.string "unused_pass", :limit => 64 + t.string "auth_header", :limit => 64, :default => "icecast-auth-user: 1" + t.string "timelimit_header", :limit => 64, :default => "icecast-auth-timelimit:" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "instruments", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "description", :limit => 1024, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.integer "popularity", :default => 0, :null => false + end + + create_table "invitations", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "sender_id", :limit => 64 + t.string "receiver_id", :limit => 64 + t.string "music_session_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "join_request_id", :limit => 64 + end + + create_table "invited_users", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "sender_id", :limit => 64 + t.boolean "autofriend", :null => false + t.string "email", :limit => 256 + t.string "invitation_code", :limit => 256, :null => false + t.boolean "accepted", :default => false + t.text "note" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "invite_medium", :limit => 64 + end + + add_index "invited_users", ["invitation_code"], :name => "invited_users_invitation_code_key", :unique => true + + create_table "isp_score_batch", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.text "json_scoring_data", :null => false + t.datetime "created_at", :null => false + end + + create_table "jam_track_licensors", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "name", :limit => nil, :null => false + t.text "description" + t.text "attention" + t.string "address_line_1", :limit => nil + t.string "address_line_2", :limit => nil + t.string "city", :limit => nil + t.string "state", :limit => nil + t.string "zip_code", :limit => nil + t.string "contact", :limit => nil + t.string "email", :limit => nil + t.string "phone", :limit => nil + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "jam_track_licensors", ["name"], :name => "jam_track_licensors_name_key", :unique => true + + create_table "jam_track_rights", :force => true do |t| + t.string "user_id", :limit => 64, :null => false + t.integer "jam_track_id", :limit => 8, :null => false + t.string "url", :limit => 2048 + t.string "md5", :limit => nil + t.integer "length", :default => 0, :null => false + t.integer "download_count", :default => 0, :null => false + t.boolean "signed", :default => false, :null => false + t.boolean "downloaded_since_sign", :default => false, :null => false + t.datetime "last_signed_at" + t.datetime "last_downloaded_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "private_key", :limit => nil + t.datetime "signing_queued_at" + t.datetime "signing_started_at" + t.integer "error_count", :default => 0, :null => false + t.string "error_reason", :limit => nil + t.string "error_detail", :limit => nil + t.boolean "should_retry", :default => false, :null => false + t.boolean "redeemed", :default => false, :null => false + end + + add_index "jam_track_rights", ["user_id", "jam_track_id"], :name => "jam_tracks_rights_uniqkey" + + create_table "jam_track_tap_ins", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.integer "offset_time", :null => false + t.integer "jam_track_id", :limit => 8, :null => false + t.decimal "bpm", :null => false + t.integer "tap_in_count", :default => 0, :null => false + end + + create_table "jam_track_tracks", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.integer "position" + t.string "track_type", :limit => nil + t.integer "jam_track_id", :limit => 8, :null => false + t.string "instrument_id", :limit => 64 + t.string "part", :limit => nil + t.string "url", :limit => nil + t.string "md5", :limit => nil + t.integer "length", :limit => 8 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "jam_track_tracks", ["position", "jam_track_id"], :name => "jam_track_tracks_position_uniqkey" + + create_table "jam_tracks", :force => true do |t| + t.string "name", :limit => nil, :null => false + t.text "description" + t.string "time_signature", :limit => nil + t.string "status", :limit => nil + t.string "recording_type", :limit => nil + t.text "original_artist" + t.text "songwriter" + t.text "publisher" + t.string "pro", :limit => nil + t.string "sales_region", :limit => nil + t.decimal "price" + t.boolean "reproduction_royalty" + t.boolean "public_performance_royalty" + t.decimal "reproduction_royalty_amount" + t.decimal "licensor_royalty_amount" + t.decimal "pro_royalty_amount" + t.string "url", :limit => nil + t.string "md5", :limit => nil + t.integer "length", :limit => 8 + t.string "licensor_id", :limit => 64 + t.string "genre_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "plan_code", :limit => 50 + t.decimal "initial_play_silence", :default => 5.0, :null => false + t.boolean "available", :default => false, :null => false + end + + add_index "jam_tracks", ["name"], :name => "jam_tracks_name_key", :unique => true + + create_table "jamcompany", :primary_key => "coid", :force => true do |t| + t.string "company", :limit => 50, :null => false + end + + add_index "jamcompany", ["company"], :name => "jamcompany_company_ndx", :unique => true + +# Could not dump table "jamisp" because of following StandardError +# Unknown type 'geometry(Polygon)' for column 'geom' + + create_table "join_requests", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64 + t.string "music_session_id", :limit => 64 + t.string "text", :limit => 2000 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "join_requests", ["user_id", "music_session_id"], :name => "user_music_session_uniqkey", :unique => true + + create_table "latency_testers", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "client_id", :limit => 64, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "latency_testers", ["client_id"], :name => "latency_testers_client_id_key", :unique => true + + create_table "layer", :id => false, :force => true do |t| + t.integer "topology_id", :null => false + t.integer "layer_id", :null => false + t.string "schema_name", :limit => nil, :null => false + t.string "table_name", :limit => nil, :null => false + t.string "feature_column", :limit => nil, :null => false + t.integer "feature_type", :null => false + t.integer "level", :default => 0, :null => false + t.integer "child_id" + end + + add_index "layer", ["schema_name", "table_name", "feature_column"], :name => "layer_schema_name_table_name_feature_column_key", :unique => true + + create_table "likes", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64, :null => false + t.string "likable_id", :limit => 64, :null => false + t.string "likable_type", :limit => 25, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "likes", ["user_id", "likable_id"], :name => "likes_user_uniqkey", :unique => true + + create_table "loader_lookuptables", :id => false, :force => true do |t| + t.integer "process_order", :default => 1000, :null => false + t.text "lookup_name", :null => false + t.text "table_name" + t.boolean "single_mode", :default => true, :null => false + t.boolean "load", :default => true, :null => false + t.boolean "level_county", :default => false, :null => false + t.boolean "level_state", :default => false, :null => false + t.boolean "level_nation", :default => false, :null => false + t.text "post_load_process" + t.boolean "single_geom_mode", :default => false + t.string "insert_mode", :limit => 1, :default => "c", :null => false + t.text "pre_load_process" + t.text "columns_exclude", :array => true + t.text "website_root_override" + end + + create_table "loader_platform", :id => false, :force => true do |t| + t.string "os", :limit => 50, :null => false + t.text "declare_sect" + t.text "pgbin" + t.text "wget" + t.text "unzip_command" + t.text "psql" + t.text "path_sep" + t.text "loader" + t.text "environ_set_command" + t.text "county_process_command" + end + + create_table "loader_variables", :id => false, :force => true do |t| + t.string "tiger_year", :limit => 4, :null => false + t.text "website_root" + t.text "staging_fold" + t.text "data_schema" + t.text "staging_schema" + end + + create_table "max_mind_releases", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.date "released_at", :null => false + t.boolean "imported", :default => false, :null => false + t.date "imported_at" + t.string "geo_ip_124_url", :limit => 2000 + t.string "geo_ip_124_md5" + t.integer "geo_ip_124_size" + t.string "geo_ip_134_url", :limit => 2000 + t.string "geo_ip_134_md5" + t.integer "geo_ip_134_size" + t.string "region_codes_url", :limit => 2000 + t.string "region_codes_md5" + t.integer "region_codes_size" + t.string "iso3166_url", :limit => 2000 + t.string "iso3166_md5" + t.integer "iso3166_size" + t.string "table_dumps_url", :limit => 2000 + t.string "table_dumps_md5" + t.integer "table_dumps_size" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "max_mind_releases", ["released_at"], :name => "max_mind_releases_released_at_key", :unique => true + + create_table "mixes", :force => true do |t| + t.string "recording_id", :limit => 64, :null => false + t.string "mix_server", :limit => 64 + t.datetime "started_at" + t.datetime "completed_at" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "ogg_md5", :limit => 100 + t.integer "ogg_length" + t.string "ogg_url", :limit => 1024 + t.boolean "completed", :default => false, :null => false + t.integer "error_count", :default => 0, :null => false + t.text "error_reason" + t.text "error_detail" + t.boolean "should_retry", :default => false, :null => false + t.string "mp3_md5", :limit => 100 + t.integer "mp3_length" + t.string "mp3_url", :limit => 1024 + t.integer "download_count", :default => 0, :null => false + t.datetime "last_downloaded_at" + end + + add_index "mixes", ["completed_at"], :name => "index_completed_at" + add_index "mixes", ["started_at"], :name => "index_started_at" + + create_table "music_notations", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64, :null => false + t.string "music_session_id", :limit => 64 + t.string "file_url", :limit => 512 + t.integer "size" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "file_name" + end + + create_table "music_session_perf_data", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "music_session_id", :limit => 64 + t.string "client_id", :limit => 64 + t.string "uri", :limit => 1000 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "music_sessions", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "music_session_id", :limit => 64 + t.string "description", :limit => 8000 + t.string "user_id", :limit => 64, :null => false + t.string "band_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "session_removed_at" + t.integer "play_count", :default => 0, :null => false + t.integer "like_count", :default => 0, :null => false + t.boolean "fan_access", :default => true, :null => false + t.datetime "scheduled_start" + t.string "scheduled_duration", :limit => nil + t.boolean "musician_access", :default => true, :null => false + t.boolean "approval_required", :default => false, :null => false + t.boolean "fan_chat", :default => true, :null => false + t.string "genre_id", :limit => 64, :null => false + t.string "legal_policy", :default => "standard", :null => false + t.string "language", :default => "eng", :null => false + t.text "name", :null => false + t.string "recurring_session_id", :limit => 64 + t.string "recurring_mode", :limit => 50, :default => "once", :null => false + t.string "timezone" + t.datetime "started_at" + t.boolean "open_rsvps", :default => false, :null => false + t.boolean "next_session_scheduled", :default => false + t.tsvector "description_tsv" + t.boolean "is_unstructured_rsvp", :default => false + t.boolean "canceled", :default => false + t.string "create_type", :limit => 64 + end + + add_index "music_sessions", ["description_tsv"], :name => "music_sessions_description_tsv_index", :using => :gin + add_index "music_sessions", ["music_session_id"], :name => "music_session_uniqkey", :unique => true + + create_table "music_sessions_comments", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "creator_id", :limit => 64, :null => false + t.string "comment", :limit => 4000, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.inet "ip_address" + t.string "music_session_id", :limit => 64 + end + + create_table "music_sessions_likers", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "liker_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.inet "ip_address" + t.string "music_session_id", :limit => 64 + end + + create_table "music_sessions_user_history", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64, :null => false + t.string "client_id", :limit => 64, :null => false + t.datetime "created_at", :null => false + t.datetime "session_removed_at" + t.integer "max_concurrent_connections" + t.integer "rating" + t.string "instruments" + t.text "rating_comment" + t.string "music_session_id", :limit => 64 + end + + add_index "music_sessions_user_history", ["music_session_id"], :name => "msuh_music_session_idx" + + create_table "musicians_instruments", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64, :null => false + t.string "instrument_id", :limit => 64, :null => false + t.integer "proficiency_level", :limit => 2, :null => false + t.integer "priority", :limit => 2, :default => 1, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "musicians_instruments", ["user_id", "instrument_id"], :name => "musician_instrument_uniqkey", :unique => true + + create_table "notifications", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "description", :limit => 100, :null => false + t.string "source_user_id", :limit => 64 + t.string "target_user_id", :limit => 64 + t.string "band_id", :limit => 64 + t.string "session_id", :limit => 64 + t.string "recording_id", :limit => 64 + t.string "invitation_id", :limit => 64 + t.string "join_request_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "friend_request_id", :limit => 64 + t.string "band_invitation_id", :limit => 64 + t.text "message" + t.integer "jam_track_right_id", :limit => 8 + end + + create_table "pagc_gaz", :force => true do |t| + t.integer "seq" + t.text "word" + t.text "stdword" + t.integer "token" + t.boolean "is_custom", :default => true, :null => false + end + + create_table "pagc_lex", :force => true do |t| + t.integer "seq" + t.text "word" + t.text "stdword" + t.integer "token" + t.boolean "is_custom", :default => true, :null => false + end + + create_table "pagc_rules", :force => true do |t| + t.text "rule" + t.boolean "is_custom", :default => true + end + + create_table "performance_samples", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64, :null => false + t.string "url", :limit => 4000 + t.string "type", :limit => 100, :null => false + t.string "claimed_recording_id", :limit => 64 + t.string "service_id", :limit => 100 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + +# Could not dump table "place" because of following StandardError +# Unknown type 'geometry' for column 'the_geom' + + create_table "place_lookup", :id => false, :force => true do |t| + t.integer "st_code", :null => false + t.string "state", :limit => 2 + t.integer "pl_code", :null => false + t.string "name", :limit => 90 + end + + add_index "place_lookup", ["state"], :name => "place_lookup_state_idx" + + create_table "playable_plays", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "playable_id", :limit => 64, :null => false + t.string "playable_type", :limit => 128, :null => false + t.string "player_id", :limit => 64 + t.string "claimed_recording_id", :limit => 64 + t.inet "ip_address" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "promotionals", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "type", :limit => 128, :default => "JamRuby::PromoBuzz", :null => false + t.string "aasm_state", :limit => 64, :default => "hidden" + t.integer "position", :default => 0, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "latest_id", :limit => 64 + t.string "latest_type", :limit => 128 + t.string "image", :limit => 1024 + t.string "text_short", :limit => 512 + t.string "text_long", :limit => 4096 + end + + add_index "promotionals", ["latest_id", "latest_type"], :name => "promo_latest_idx" + + create_table "quick_mixes", :force => true do |t| + t.integer "next_part_to_upload", :default => 0, :null => false + t.boolean "fully_uploaded", :default => false, :null => false + t.string "upload_id", :limit => 1024 + t.integer "file_offset", :limit => 8, :default => 0 + t.boolean "is_part_uploading", :default => false, :null => false + t.integer "upload_failures", :default => 0 + t.integer "part_failures", :default => 0 + t.string "ogg_md5", :limit => 100 + t.integer "ogg_length" + t.string "ogg_url", :limit => 1000 + t.string "mp3_md5", :limit => 100 + t.integer "mp3_length" + t.string "mp3_url", :limit => 1000 + t.integer "error_count", :default => 0, :null => false + t.text "error_reason" + t.text "error_detail" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.datetime "started_at" + t.datetime "completed_at" + t.boolean "completed", :default => false, :null => false + t.boolean "should_retry", :default => false, :null => false + t.boolean "cleaned", :default => false, :null => false + t.string "user_id", :limit => 64 + t.string "recording_id", :limit => 64 + end + + create_table "recorded_backing_tracks", :force => true do |t| + t.string "user_id", :limit => 64 + t.string "backing_track_id", :limit => 64 + t.string "recording_id", :limit => 64, :null => false + t.string "client_track_id", :limit => 64, :null => false + t.boolean "is_part_uploading", :default => false, :null => false + t.integer "next_part_to_upload", :default => 0, :null => false + t.string "upload_id", :limit => 1024 + t.integer "part_failures", :default => 0, :null => false + t.boolean "discard" + t.integer "download_count", :default => 0, :null => false + t.string "md5", :limit => 100 + t.integer "length", :limit => 8 + t.string "client_id", :limit => 64, :null => false + t.integer "file_offset", :limit => 8 + t.string "url", :limit => 1024, :null => false + t.boolean "fully_uploaded", :default => false, :null => false + t.integer "upload_failures", :default => 0, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "filename", :limit => nil, :null => false + t.datetime "last_downloaded_at" + end + + create_table "recorded_tracks", :force => true do |t| + t.string "user_id", :limit => 64, :null => false + t.string "instrument_id", :limit => 64, :null => false + t.string "sound", :limit => 64, :null => false + t.integer "next_part_to_upload", :default => 0, :null => false + t.boolean "fully_uploaded", :default => false, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "upload_id", :limit => 1024 + t.string "recording_id", :limit => 64, :null => false + t.string "md5", :limit => 100 + t.integer "length", :limit => 8 + t.string "client_id", :limit => 64, :null => false + t.string "track_id", :limit => 64, :null => false + t.string "url", :limit => 1024 + t.integer "file_offset", :limit => 8, :default => 0 + t.string "client_track_id", :limit => 64, :null => false + t.boolean "is_part_uploading", :default => false, :null => false + t.integer "upload_failures", :default => 0, :null => false + t.integer "part_failures", :default => 0, :null => false + t.boolean "discard" + t.integer "download_count", :default => 0, :null => false + t.datetime "last_downloaded_at" + end + + create_table "recorded_videos", :force => true do |t| + t.string "user_id", :limit => 64 + t.boolean "fully_uploaded", :default => false, :null => false + t.string "recording_id", :limit => 64, :null => false + t.integer "length", :limit => 8 + t.string "client_video_source_id", :limit => 64, :null => false + t.string "url", :limit => 1024 + t.integer "file_offset", :limit => 8 + t.integer "upload_failures", :default => 0, :null => false + t.boolean "discard" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "recordings", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "owner_id", :limit => 64, :null => false + t.string "music_session_id", :limit => 64 + t.string "band_id", :limit => 64 + t.integer "duration" + t.boolean "is_done", :default => false + t.boolean "all_discarded", :default => false, :null => false + t.string "name", :limit => 1024 + t.integer "play_count", :default => 0, :null => false + t.integer "like_count", :default => 0, :null => false + t.boolean "has_stream_mix", :default => false, :null => false + t.boolean "has_final_mix", :default => false, :null => false + t.integer "first_quick_mix_id", :limit => 8 + t.boolean "deleted", :default => false, :null => false + end + + create_table "recordings_comments", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "recording_id", :limit => 64, :null => false + t.string "creator_id", :limit => 64, :null => false + t.string "comment", :limit => 4000, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.inet "ip_address" + end + + create_table "recordings_downloads", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "recording_id", :limit => 64, :null => false + t.string "downloader_id", :limit => 64, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "recordings_likers", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "recording_id", :limit => 64, :null => false + t.string "liker_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.inet "ip_address" + t.string "claimed_recording_id", :limit => 64, :null => false + t.boolean "favorite", :default => true, :null => false + end + + add_index "recordings_likers", ["recording_id", "liker_id"], :name => "recording_liker_uniqkey", :unique => true + + create_table "recurring_sessions", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "description", :limit => 8000 + t.datetime "scheduled_start" + t.string "scheduled_duration", :limit => nil + t.boolean "musician_access", :null => false + t.boolean "approval_required", :null => false + t.boolean "fan_chat", :null => false + t.string "genre_id", :limit => 64 + t.string "legal_policy", :default => "standard", :null => false + t.string "language", :default => "en", :null => false + t.text "name" + t.string "user_id", :limit => 64, :null => false + t.string "band_id", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "regions", :id => false, :force => true do |t| + t.string "region", :limit => 2, :null => false + t.string "regionname", :limit => 64 + t.string "countrycode", :limit => 2, :null => false + end + + add_index "regions", ["countrycode", "region"], :name => "regions_countrycode_region_ndx", :unique => true + add_index "regions", ["countrycode"], :name => "regions_countrycode_ndx" + + create_table "rsvp_requests", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64, :null => false + t.boolean "canceled", :default => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.boolean "cancel_all", :default => false + end + + create_table "rsvp_requests_rsvp_slots", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "rsvp_request_id", :limit => 64, :null => false + t.string "rsvp_slot_id", :limit => 64, :null => false + t.boolean "chosen" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "rsvp_slots", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "instrument_id", :limit => 64 + t.integer "proficiency_level", :limit => 2 + t.string "music_session_id", :limit => 64, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.boolean "is_unstructured_rsvp", :default => false + end + + create_table "score_histories", :id => false, :force => true do |t| + t.string "from_client_id", :limit => 64 + t.integer "from_addr", :limit => 8 + t.string "from_isp", :limit => 50 + t.string "from_country", :limit => 64 + t.string "from_region", :limit => 64 + t.string "from_city" + t.string "from_postal", :limit => 25 + t.float "from_latitude" + t.float "from_longitude" + t.string "to_client_id", :limit => 64 + t.integer "to_addr", :limit => 8 + t.string "to_isp", :limit => 50 + t.string "to_country", :limit => 64 + t.string "to_region", :limit => 64 + t.string "to_city" + t.string "to_postal", :limit => 25 + t.float "to_latitude" + t.float "to_longitude" + t.integer "score", :null => false + t.datetime "score_dt", :null => false + t.text "scoring_data" + t.string "from_user_id", :limit => 64 + t.string "to_user_id", :limit => 64 + t.string "from_latency_tester_id", :limit => 64 + t.string "to_latency_tester_id", :limit => 64 + t.integer "from_locidispid", :limit => 8, :null => false + t.integer "to_locidispid", :limit => 8, :null => false + end + + create_table "scores", :id => false, :force => true do |t| + t.integer "alocidispid", :limit => 8, :null => false + t.string "anodeid", :limit => 64, :null => false + t.integer "aaddr", :limit => 8, :null => false + t.integer "blocidispid", :limit => 8, :null => false + t.string "bnodeid", :limit => 64, :null => false + t.integer "baddr", :limit => 8, :null => false + t.integer "score", :null => false + t.integer "scorer", :null => false + t.datetime "score_dt", :null => false + t.string "scoring_data", :limit => 4000 + t.datetime "created_at", :null => false + t.string "auserid", :limit => 64 + t.string "buserid", :limit => 64 + t.string "alatencytestid", :limit => 64 + t.string "blatencytestid", :limit => 64 + end + + add_index "scores", ["alocidispid", "blocidispid", "score_dt"], :name => "scores_alocidispid_blocidispid_score_dt_ndx" + add_index "scores", ["blocidispid", "alocidispid", "score_dt"], :name => "scores_blocidispid_alocidispid_score_dt_ndx" + + create_table "secondary_unit_lookup", :id => false, :force => true do |t| + t.string "name", :limit => 20, :null => false + t.string "abbrev", :limit => 5 + end + + add_index "secondary_unit_lookup", ["abbrev"], :name => "secondary_unit_lookup_abbrev_idx" + + create_table "session_info_comments", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "music_session_id", :limit => 64, :null => false + t.string "creator_id", :limit => 64, :null => false + t.text "comment", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "share_tokens", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "token", :limit => 15, :null => false + t.string "shareable_id", :limit => 64, :null => false + t.string "shareable_type", :limit => 50, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "share_tokens", ["token"], :name => "token_uniqkey", :unique => true + + create_table "shopping_carts", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.integer "quantity", :default => 1, :null => false + t.string "user_id", :limit => 64, :null => false + t.string "cart_id", :limit => 64, :null => false + t.string "cart_class_name", :limit => 64 + t.string "cart_type", :limit => 64 + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "spatial_ref_sys", :id => false, :force => true do |t| + t.integer "srid", :null => false + t.string "auth_name", :limit => 256 + t.integer "auth_srid" + t.string "srtext", :limit => 2048 + t.string "proj4text", :limit => 2048 + end + +# Could not dump table "state" because of following StandardError +# Unknown type 'geometry' for column 'the_geom' + + create_table "state_lookup", :id => false, :force => true do |t| + t.integer "st_code", :null => false + t.string "name", :limit => 40 + t.string "abbrev", :limit => 3 + t.string "statefp", :limit => 2 + end + + add_index "state_lookup", ["abbrev"], :name => "state_lookup_abbrev_key", :unique => true + add_index "state_lookup", ["name"], :name => "state_lookup_name_key", :unique => true + add_index "state_lookup", ["statefp"], :name => "state_lookup_statefp_key", :unique => true + + create_table "street_type_lookup", :id => false, :force => true do |t| + t.string "name", :limit => 50, :null => false + t.string "abbrev", :limit => 50 + t.boolean "is_hw", :default => false, :null => false + end + + add_index "street_type_lookup", ["abbrev"], :name => "street_type_lookup_abbrev_idx" + +# Could not dump table "tabblock" because of following StandardError +# Unknown type 'geometry' for column 'the_geom' + + create_table "text_messages", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "source_user_id", :limit => 64 + t.string "target_user_id", :limit => 64 + t.text "message", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "topology", :force => true do |t| + t.string "name", :limit => nil, :null => false + t.integer "srid", :null => false + t.float "precision", :null => false + t.boolean "hasz", :default => false, :null => false + end + + add_index "topology", ["name"], :name => "topology_name_key", :unique => true + + create_table "tracks", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "connection_id", :limit => 64, :null => false + t.string "instrument_id", :limit => 64 + t.string "sound", :limit => 64, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "client_track_id", :limit => 64, :null => false + t.string "client_resource_id", :limit => 100 + end + +# Could not dump table "tract" because of following StandardError +# Unknown type 'geometry' for column 'the_geom' + + create_table "user_authorizations", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64 + t.string "uid", :null => false + t.string "provider", :null => false + t.string "token", :limit => 2000 + t.datetime "token_expiration" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + t.string "secret" + end + + add_index "user_authorizations", ["provider", "uid"], :name => "user_authorizations_uniqkey", :unique => true + add_index "user_authorizations", ["user_id"], :name => "user_authorizations_user_id_idx" + + create_table "user_presences", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "user_id", :limit => 64, :null => false + t.string "type", :limit => 100, :null => false + t.string "username", :limit => 100, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + +# Could not dump table "users" because of following StandardError +# Unknown type 'json' for column 'mods' + + create_table "video_sources", :id => false, :force => true do |t| + t.string "id", :limit => 64, :null => false + t.string "connection_id", :limit => 64, :null => false + t.string "client_video_source_id", :limit => 64, :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + create_table "wert_snot", :id => false, :force => true do |t| + t.integer "count", :limit => 8 + t.integer "last_jam_locidispid", :limit => 8 + end + +# Could not dump table "zcta5" because of following StandardError +# Unknown type 'geometry' for column 'the_geom' + + create_table "zip_lookup", :id => false, :force => true do |t| + t.integer "zip", :null => false + t.integer "st_code" + t.string "state", :limit => 2 + t.integer "co_code" + t.string "county", :limit => 90 + t.integer "cs_code" + t.string "cousub", :limit => 90 + t.integer "pl_code" + t.string "place", :limit => 90 + t.integer "cnt" + end + + create_table "zip_lookup_all", :id => false, :force => true do |t| + t.integer "zip" + t.integer "st_code" + t.string "state", :limit => 2 + t.integer "co_code" + t.string "county", :limit => 90 + t.integer "cs_code" + t.string "cousub", :limit => 90 + t.integer "pl_code" + t.string "place", :limit => 90 + t.integer "cnt" + end + + create_table "zip_lookup_base", :id => false, :force => true do |t| + t.string "zip", :limit => 5, :null => false + t.string "state", :limit => 40 + t.string "county", :limit => 90 + t.string "city", :limit => 90 + t.string "statefp", :limit => 2 + end + + create_table "zip_state", :id => false, :force => true do |t| + t.string "zip", :limit => 5, :null => false + t.string "stusps", :limit => 2, :null => false + t.string "statefp", :limit => 2 + end + + create_table "zip_state_loc", :id => false, :force => true do |t| + t.string "zip", :limit => 5, :null => false + t.string "stusps", :limit => 2, :null => false + t.string "statefp", :limit => 2 + t.string "place", :limit => 100, :null => false + end + +end diff --git a/web/db/structure.sql b/web/db/structure.sql new file mode 100644 index 000000000..4a3422815 --- /dev/null +++ b/web/db/structure.sql @@ -0,0 +1,3862 @@ +-- +-- PostgreSQL database dump +-- + +SET statement_timeout = 0; +SET lock_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SET check_function_bodies = false; +SET client_min_messages = warning; + +-- +-- Name: pgmigrate; Type: SCHEMA; Schema: -; Owner: - +-- + +CREATE SCHEMA pgmigrate; + + +-- +-- Name: tiger; Type: SCHEMA; Schema: -; Owner: - +-- + +CREATE SCHEMA tiger; + + +-- +-- Name: fuzzystrmatch; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS fuzzystrmatch WITH SCHEMA public; + + +-- +-- Name: EXTENSION fuzzystrmatch; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION fuzzystrmatch IS 'determine similarities and distance between strings'; + + +-- +-- Name: postgis; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA public; + + +-- +-- Name: EXTENSION postgis; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION postgis IS 'PostGIS geometry, geography, and raster spatial types and functions'; + + +-- +-- Name: postgis_tiger_geocoder; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS postgis_tiger_geocoder WITH SCHEMA tiger; + + +-- +-- Name: EXTENSION postgis_tiger_geocoder; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION postgis_tiger_geocoder IS 'PostGIS tiger geocoder and reverse geocoder'; + + +-- +-- Name: topology; Type: SCHEMA; Schema: -; Owner: - +-- + +CREATE SCHEMA topology; + + +-- +-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; + + +-- +-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; + + +-- +-- Name: postgis_topology; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS postgis_topology WITH SCHEMA topology; + + +-- +-- Name: EXTENSION postgis_topology; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION postgis_topology IS 'PostGIS topology spatial types and functions'; + + +-- +-- Name: uuid-ossp; Type: EXTENSION; Schema: -; Owner: - +-- + +CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA public; + + +-- +-- Name: EXTENSION "uuid-ossp"; Type: COMMENT; Schema: -; Owner: - +-- + +COMMENT ON EXTENSION "uuid-ossp" IS 'generate universally unique identifiers (UUIDs)'; + + +SET search_path = pgmigrate, pg_catalog; + +-- +-- Name: bootstrap_pg_migrate(); Type: FUNCTION; Schema: pgmigrate; Owner: - +-- + +CREATE FUNCTION bootstrap_pg_migrate() RETURNS void + LANGUAGE plpgsql + AS $$ DECLARE found_pg_migrate information_schema.tables; found_pg_migrations information_schema.tables; BEGIN BEGIN SELECT * INTO STRICT found_pg_migrate FROM information_schema.tables WHERE table_name = 'pg_migrate' and table_schema = 'pgmigrate'; EXCEPTION WHEN NO_DATA_FOUND THEN CREATE TABLE pgmigrate.pg_migrate (id BIGSERIAL PRIMARY KEY, template_version VARCHAR(255), builder_version VARCHAR(255), migrator_version VARCHAR(255), database_version VARCHAR(1024)); CREATE INDEX pg_migrate_unique_index ON pgmigrate.pg_migrate (template_version, builder_version, migrator_version, database_version); WHEN TOO_MANY_ROWS THEN RAISE EXCEPTION 'Multiple pg_migrate tables. Unique key on information_schema.tables should have prevented this.'; END; BEGIN SELECT * INTO STRICT found_pg_migrations FROM information_schema.tables WHERE table_name = 'pg_migrations' and table_schema = 'pgmigrate'; EXCEPTION WHEN NO_DATA_FOUND THEN CREATE TABLE pgmigrate.pg_migrations( name VARCHAR(255) PRIMARY KEY, ordinal INTEGER NOT NULL, created TIMESTAMP DEFAULT CURRENT_TIMESTAMP, finalized SMALLINT DEFAULT 1, pg_migrate_id BIGINT NOT NULL REFERENCES pgmigrate.pg_migrate(id)); WHEN TOO_MANY_ROWS THEN RAISE EXCEPTION 'Multiple pg_migrations tables. Unique key on information_schema.tables should have prevented this.'; END; END; $$; + + +-- +-- Name: bypass_existing_migration(character varying); Type: FUNCTION; Schema: pgmigrate; Owner: - +-- + +CREATE FUNCTION bypass_existing_migration(migration character varying) RETURNS void + LANGUAGE plpgsql + AS $_$ DECLARE found_migration pgmigrate.pg_migrations; BEGIN BEGIN EXECUTE 'SELECT * FROM pgmigrate.pg_migrations WHERE name=$1' INTO STRICT found_migration USING migration ; EXCEPTION WHEN NO_DATA_FOUND THEN RETURN; WHEN TOO_MANY_ROWS THEN RAISE EXCEPTION 'pg_migrate: code=pg_migrations_uniqueness_error, migration=%', migration; END; RAISE EXCEPTION 'pg_migrate: code=migration_exists, migration=%', migration; END; $_$; + + +-- +-- Name: record_migration(character varying, integer, character varying, character varying); Type: FUNCTION; Schema: pgmigrate; Owner: - +-- + +CREATE FUNCTION record_migration(migration character varying, ordinal integer, template_version character varying, builder_version character varying) RETURNS void + LANGUAGE plpgsql + AS $_$ DECLARE found_pg_migrate_id BIGINT; migrator_version VARCHAR(255); BEGIN EXECUTE 'SELECT current_setting(''application_name'')' INTO migrator_version; BEGIN EXECUTE 'SELECT id FROM pgmigrate.pg_migrate WHERE template_version=$1 and builder_version=$2 and migrator_version=$3 and database_version=$4' INTO found_pg_migrate_id USING template_version, builder_version, migrator_version, (select version()); EXCEPTION WHEN NO_DATA_FOUND THEN found_pg_migrate_id = NULL; WHEN TOO_MANY_ROWS THEN RAISE EXCEPTION 'pg_migrate: code=pg_migrate_uniqueness_error, migration=%, ordinal=%, template_version=%, builder_version=%, migrator_version=%, database_version', migration, ordinal, template_version, builder_version, migrator_version, (select version()); END; IF found_pg_migrate_id IS NULL THEN INSERT INTO pgmigrate.pg_migrate(id, template_version, builder_version, migrator_version, database_version) VALUES (default, template_version, builder_version, migrator_version, (select version())) RETURNING id INTO found_pg_migrate_id; END IF; EXECUTE 'INSERT INTO pgmigrate.pg_migrations(name, ordinal, created, finalized, pg_migrate_id) VALUES ($1, $2, CURRENT_TIMESTAMP, 1, $3)' USING migration, ordinal, found_pg_migrate_id; END; $_$; + + +-- +-- Name: verify_against_existing_migrations(character varying, integer); Type: FUNCTION; Schema: pgmigrate; Owner: - +-- + +CREATE FUNCTION verify_against_existing_migrations(migration character varying, ordinal integer) RETURNS void + LANGUAGE plpgsql + AS $_$ DECLARE found_migration pgmigrate.pg_migrations; BEGIN BEGIN EXECUTE 'SELECT * FROM pgmigrate.pg_migrations WHERE name=$1' INTO STRICT found_migration USING migration; EXCEPTION WHEN NO_DATA_FOUND THEN IF coalesce((SELECT MAX(p.ordinal) FROM pgmigrate.pg_migrations as p), -1) <> ordinal - 1 THEN RAISE EXCEPTION 'pg_migrate: code=missing_migration, migration=%, ordinal=%, last_ordinal=%', migration, ordinal, (SELECT MAX(p.ordinal) FROM pgmigrate.pg_migrations as p); END IF; RETURN; WHEN TOO_MANY_ROWS THEN RAISE EXCEPTION 'pg_migrate: code=pg_migrations_uniqueness_error, migration=%', migration; END; IF found_migration.ordinal <> ordinal THEN RAISE EXCEPTION 'pg_migrate: code=incorrect_ordinal, migration=%, expected_ordinal=%, actual_ordinal', migration, ordinal, found_migration.ordinal; END IF; END; $_$; + + +-- +-- Name: verify_manifest_is_not_old(integer); Type: FUNCTION; Schema: pgmigrate; Owner: - +-- + +CREATE FUNCTION verify_manifest_is_not_old(manifest_version integer) RETURNS void + LANGUAGE plpgsql + AS $$ DECLARE max_ordinal INTEGER; BEGIN EXECUTE 'SELECT max(ordinal) FROM pgmigrate.pg_migrations' INTO max_ordinal; IF max_ordinal > manifest_version THEN RAISE EXCEPTION 'pg_migrate: code=old_manifest, max_ordinal_in_db=%, manifest_version=%', max_ordinal, manifest_version; END IF; END; $$; + + +SET search_path = public, pg_catalog; + +-- +-- Name: bootstrap_users(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION bootstrap_users() RETURNS void + LANGUAGE plpgsql + AS $_$ DECLARE test_user VARCHAR(64); BEGIN SELECT id INTO STRICT test_user FROM users WHERE id = '1'; UPDATE users SET first_name = 'Test', last_name = 'User', email = 'test@jamkazam.com', remember_token = 'NQubl-z16Em94tnSdofObw', password_digest = '$2a$10$QyaNTLVX5DAaJ.JL21kDWeUQqdh3Qh7JQbdRgE82x1Cib7HWNcHXC', email_confirmed=true, musician=true WHERE id = '1'; EXCEPTION WHEN NO_DATA_FOUND THEN INSERT INTO users (id, first_name, last_name, email, remember_token, password_digest, email_confirmed, musician) VALUES ('1', 'Test', 'User', 'test@jamkazam.com', 'NQubl-z16Em94tnSdofObw', '$2a$10$QyaNTLVX5DAaJ.JL21kDWeUQqdh3Qh7JQbdRgE82x1Cib7HWNcHXC', true, true); RETURN; WHEN TOO_MANY_ROWS THEN RAISE EXCEPTION 'user id 1 not unique'; END; $_$; + + +-- +-- Name: get_work(bigint); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION get_work(mylocidispid bigint) RETURNS TABLE(client_id character varying) + LANGUAGE plpgsql ROWS 5 + AS $$ BEGIN CREATE TEMPORARY TABLE foo (locidispid BIGINT, locid INT); INSERT INTO foo SELECT DISTINCT locidispid, locidispid/1000000 FROM connections where client_type = 'client'; DELETE FROM foo WHERE locidispid IN (SELECT DISTINCT blocidispid FROM current_scores WHERE alocidispid = mylocidispid AND (current_timestamp - score_dt) < interval '24 hours'); DELETE FROM foo WHERE locid NOT IN (SELECT locid FROM geoiplocations WHERE geog && st_buffer((SELECT geog from geoiplocations WHERE locid = mylocidispid/1000000), 806000)); CREATE TEMPORARY TABLE bar (client_id VARCHAR(64), locidispid BIGINT, r DOUBLE PRECISION); INSERT INTO bar SELECT l.client_id, l.locidispid, random() FROM connections l, foo f WHERE l.locidispid = f.locidispid and l.client_type = 'client'; DROP TABLE foo; DELETE FROM bar b WHERE r != (SELECT max(r) FROM bar b0 WHERE b0.locidispid = b.locidispid); RETURN QUERY SELECT b.client_id FROM bar b ORDER BY r LIMIT 5; DROP TABLE bar; RETURN; END; $$; + + +-- +-- Name: truncate_tables(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION truncate_tables() RETURNS void + LANGUAGE plpgsql + AS $$ DECLARE statements CURSOR FOR SELECT tablename FROM pg_tables WHERE schemaname = 'public'; BEGIN FOR stmt IN statements LOOP EXECUTE 'TRUNCATE TABLE ' || quote_ident(stmt.tablename) || ' CASCADE;'; END LOOP; END; $$; + + +-- +-- Name: english_stem; Type: TEXT SEARCH DICTIONARY; Schema: public; Owner: - +-- + +CREATE TEXT SEARCH DICTIONARY english_stem ( + TEMPLATE = pg_catalog.snowball, + language = 'english', stopwords = 'english' ); + + +-- +-- Name: jamenglish; Type: TEXT SEARCH CONFIGURATION; Schema: public; Owner: - +-- + +CREATE TEXT SEARCH CONFIGURATION jamenglish ( + PARSER = pg_catalog."default" ); + +ALTER TEXT SEARCH CONFIGURATION jamenglish + ADD MAPPING FOR asciiword WITH pg_catalog.english_stem; + +ALTER TEXT SEARCH CONFIGURATION jamenglish + ADD MAPPING FOR word WITH pg_catalog.english_stem; + +ALTER TEXT SEARCH CONFIGURATION jamenglish + ADD MAPPING FOR numword WITH simple; + +ALTER TEXT SEARCH CONFIGURATION jamenglish + ADD MAPPING FOR host WITH simple; + +ALTER TEXT SEARCH CONFIGURATION jamenglish + ADD MAPPING FOR version WITH simple; + +ALTER TEXT SEARCH CONFIGURATION jamenglish + ADD MAPPING FOR hword_numpart WITH simple; + +ALTER TEXT SEARCH CONFIGURATION jamenglish + ADD MAPPING FOR hword_part WITH pg_catalog.english_stem; + +ALTER TEXT SEARCH CONFIGURATION jamenglish + ADD MAPPING FOR hword_asciipart WITH pg_catalog.english_stem; + +ALTER TEXT SEARCH CONFIGURATION jamenglish + ADD MAPPING FOR numhword WITH simple; + +ALTER TEXT SEARCH CONFIGURATION jamenglish + ADD MAPPING FOR asciihword WITH pg_catalog.english_stem; + +ALTER TEXT SEARCH CONFIGURATION jamenglish + ADD MAPPING FOR hword WITH pg_catalog.english_stem; + +ALTER TEXT SEARCH CONFIGURATION jamenglish + ADD MAPPING FOR file WITH simple; + +ALTER TEXT SEARCH CONFIGURATION jamenglish + ADD MAPPING FOR "int" WITH simple; + +ALTER TEXT SEARCH CONFIGURATION jamenglish + ADD MAPPING FOR uint WITH simple; + + +SET search_path = pgmigrate, pg_catalog; + +SET default_tablespace = ''; + +SET default_with_oids = false; + +-- +-- Name: pg_migrate; Type: TABLE; Schema: pgmigrate; Owner: -; Tablespace: +-- + +CREATE TABLE pg_migrate ( + id bigint NOT NULL, + template_version character varying(255), + builder_version character varying(255), + migrator_version character varying(255), + database_version character varying(1024) +); + + +-- +-- Name: pg_migrate_id_seq; Type: SEQUENCE; Schema: pgmigrate; Owner: - +-- + +CREATE SEQUENCE pg_migrate_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: pg_migrate_id_seq; Type: SEQUENCE OWNED BY; Schema: pgmigrate; Owner: - +-- + +ALTER SEQUENCE pg_migrate_id_seq OWNED BY pg_migrate.id; + + +-- +-- Name: pg_migrations; Type: TABLE; Schema: pgmigrate; Owner: -; Tablespace: +-- + +CREATE TABLE pg_migrations ( + name character varying(255) NOT NULL, + ordinal integer NOT NULL, + created timestamp without time zone DEFAULT now(), + finalized smallint DEFAULT 1, + pg_migrate_id bigint NOT NULL +); + + +SET search_path = public, pg_catalog; + +-- +-- Name: active_admin_comments; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE active_admin_comments ( + id integer NOT NULL, + resource_id character varying(255) NOT NULL, + resource_type character varying(255) NOT NULL, + author_id integer, + author_type character varying(255), + body text, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + namespace character varying(255) +); + + +-- +-- Name: active_admin_comments_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE active_admin_comments_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: active_admin_comments_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE active_admin_comments_id_seq OWNED BY active_admin_comments.id; + + +-- +-- Name: active_music_sessions; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNLOGGED TABLE active_music_sessions ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + claimed_recording_id character varying(64), + claimed_recording_initiator_id character varying(64), + track_changes_counter integer DEFAULT 0 +); + + +-- +-- Name: affiliate_partners; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE affiliate_partners ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + partner_name character varying(128) NOT NULL, + partner_code character varying(128) NOT NULL, + partner_user_id character varying(64) NOT NULL, + user_email character varying(255), + referral_user_count integer DEFAULT 0 NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: artifact_updates; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE artifact_updates ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + product character varying(255) NOT NULL, + version character varying(255) NOT NULL, + uri character varying(2000) NOT NULL, + sha1 character varying(255) NOT NULL, + environment character varying(255) DEFAULT 'public'::character varying NOT NULL, + size integer NOT NULL +); + + +-- +-- Name: band_invitations; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE band_invitations ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64), + band_id character varying(64), + accepted boolean, + creator_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: bands; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE bands ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + name character varying(1024) NOT NULL, + website character varying(4000), + biography character varying(4000) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + city character varying(100), + state character varying(100), + country character varying(100), + photo_url character varying(2048), + logo_url character varying(2048), + name_tsv tsvector, + original_fpfile_photo character varying(8000) DEFAULT NULL::character varying, + cropped_fpfile_photo character varying(8000) DEFAULT NULL::character varying, + cropped_s3_path_photo character varying(512) DEFAULT NULL::character varying, + crop_selection_photo character varying(256) DEFAULT NULL::character varying, + lat numeric(15,10), + lng numeric(15,10), + large_photo_url character varying(2048), + cropped_large_s3_path_photo character varying(512), + cropped_large_fpfile_photo character varying(8000), + did_real_session boolean DEFAULT false +); + + +-- +-- Name: bands_genres; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE bands_genres ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + band_id character varying(64) NOT NULL, + genre_id character varying(64) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: bands_musicians; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE bands_musicians ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + band_id character varying(64) NOT NULL, + user_id character varying(64) NOT NULL, + admin boolean DEFAULT false NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: chat_messages; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE chat_messages ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64), + music_session_id character varying(64), + message text NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: cities; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE cities ( + city character varying(255) NOT NULL, + region character varying(2) NOT NULL, + countrycode character varying(2) NOT NULL +); + + +-- +-- Name: claimed_recordings; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE claimed_recordings ( + user_id character varying(64) NOT NULL, + recording_id character varying(64) NOT NULL, + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + name character varying(200) NOT NULL, + is_public boolean DEFAULT true NOT NULL, + genre_id character varying(64) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + description character varying(8000), + description_tsv tsvector, + name_tsv tsvector +); + + +-- +-- Name: connections; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNLOGGED TABLE connections ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64) NOT NULL, + client_id character varying(64) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + music_session_id character varying(64), + ip_address character varying(64) NOT NULL, + as_musician boolean, + aasm_state character varying(64) DEFAULT 'idle'::character varying NOT NULL, + addr bigint NOT NULL, + locidispid bigint NOT NULL, + joined_session_at timestamp without time zone, + client_type character varying(256) NOT NULL, + stale_time integer DEFAULT 40 NOT NULL, + expire_time integer DEFAULT 60 NOT NULL +); + + +-- +-- Name: countries; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE countries ( + countrycode character varying(2) NOT NULL, + countryname character varying(64) +); + + +-- +-- Name: crash_dumps; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE crash_dumps ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + client_type character varying(64) NOT NULL, + client_id character varying(64), + user_id character varying(64), + session_id character varying(64), + "timestamp" timestamp without time zone, + uri character varying(1000), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + client_version character varying(100) NOT NULL +); + + +-- +-- Name: scores; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE scores ( + alocidispid bigint NOT NULL, + anodeid character varying(64) NOT NULL, + aaddr bigint NOT NULL, + blocidispid bigint NOT NULL, + bnodeid character varying(64) NOT NULL, + baddr bigint NOT NULL, + score integer NOT NULL, + scorer integer NOT NULL, + score_dt timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: current_scores; Type: VIEW; Schema: public; Owner: - +-- + +CREATE VIEW current_scores AS + SELECT s.alocidispid, + s.anodeid, + s.aaddr, + s.blocidispid, + s.bnodeid, + s.baddr, + s.score, + s.scorer, + s.score_dt + FROM scores s + WHERE (s.score_dt = ( SELECT max(s0.score_dt) AS max + FROM scores s0 + WHERE ((s0.alocidispid = s.alocidispid) AND (s0.blocidispid = s.blocidispid)))); + + +-- +-- Name: diagnostics; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE diagnostics ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64) NOT NULL, + type character varying(255) NOT NULL, + creator character varying(255) NOT NULL, + data text, + created_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: email_batch_sets; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE email_batch_sets ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + email_batch_id character varying(64), + started_at timestamp without time zone, + user_ids text DEFAULT ''::text NOT NULL, + batch_count integer, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + trigger_index integer DEFAULT 0 NOT NULL, + sub_type character varying(64), + user_id character varying(64) +); + + +-- +-- Name: email_batches; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE email_batches ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + subject character varying(256), + body text, + from_email character varying(64) DEFAULT 'noreply@jamkazam.com'::character varying NOT NULL, + aasm_state character varying(32) DEFAULT 'pending'::character varying NOT NULL, + test_emails text DEFAULT 'test@jamkazam.com'::text NOT NULL, + opt_in_count integer DEFAULT 0 NOT NULL, + sent_count integer DEFAULT 0 NOT NULL, + lock_version integer, + started_at timestamp without time zone, + completed_at timestamp without time zone, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + type character varying(64) DEFAULT 'JamRuby::EmailBatch'::character varying NOT NULL, + sub_type character varying(64) +); + + +-- +-- Name: email_errors; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE email_errors ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64), + error_type character varying(32), + email_address character varying(256), + status character varying(32), + email_date timestamp without time zone DEFAULT now(), + reason text, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: event_sessions; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE event_sessions ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + starts_at timestamp without time zone, + ends_at timestamp without time zone, + pinned_state character varying(255), + img_url character varying(1024), + img_width integer, + img_height integer, + event_id character varying(64), + user_id character varying(64), + band_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + ordinal integer +); + + +-- +-- Name: events; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE events ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + slug character varying(512) NOT NULL, + title text, + description text, + show_sponser boolean DEFAULT false NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + social_description text +); + + +-- +-- Name: facebook_signups; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNLOGGED TABLE facebook_signups ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + lookup_id character varying(255) NOT NULL, + last_name character varying(100), + first_name character varying(100), + gender character varying(1), + email character varying(1024), + uid character varying(1024), + token character varying(1024), + token_expires_at timestamp without time zone, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: fan_invitations; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE fan_invitations ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + sender_id character varying(64), + receiver_id character varying(64), + music_session_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: feeds; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE feeds ( + id bigint NOT NULL, + recording_id character varying(64), + music_session_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: feeds_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE feeds_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: feeds_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE feeds_id_seq OWNED BY feeds.id; + + +-- +-- Name: follows; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE follows ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64) NOT NULL, + followable_id character varying(64) NOT NULL, + followable_type character varying(25) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: friend_requests; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE friend_requests ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64), + friend_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + status character varying(50), + message character varying(4000) +); + + +-- +-- Name: friendships; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE friendships ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64), + friend_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: genres; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE genres ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + description character varying(1024) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: genres_music_sessions; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNLOGGED TABLE genres_music_sessions ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + genre_id character varying(64), + music_session_id character varying(64) +); + + +-- +-- Name: geoipblocks; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE geoipblocks ( + beginip bigint NOT NULL, + endip bigint NOT NULL, + locid integer NOT NULL, + geom geometry(Polygon) +); + + +-- +-- Name: geoipisp; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE geoipisp ( + beginip bigint NOT NULL, + endip bigint NOT NULL, + company character varying(50) NOT NULL +); + + +-- +-- Name: geoiplocations; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE geoiplocations ( + locid integer NOT NULL, + countrycode character varying(2), + region character varying(2), + city character varying(255), + postalcode character varying(8), + latitude double precision NOT NULL, + longitude double precision NOT NULL, + metrocode integer, + areacode character(3), + geog geography(Point,4326) +); + + +-- +-- Name: icecast_admin_authentications; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_admin_authentications ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + source_pass character varying(64) NOT NULL, + relay_user character varying(64) NOT NULL, + relay_pass character varying(64) NOT NULL, + admin_user character varying(64) NOT NULL, + admin_pass character varying(64) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_directories; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_directories ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + yp_url_timeout integer DEFAULT 15 NOT NULL, + yp_url character varying(1024) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_limits; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_limits ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + clients integer DEFAULT 1000 NOT NULL, + sources integer DEFAULT 50 NOT NULL, + queue_size integer DEFAULT 102400 NOT NULL, + client_timeout integer DEFAULT 30, + header_timeout integer DEFAULT 15, + source_timeout integer DEFAULT 10, + burst_size integer DEFAULT 65536, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_listen_sockets; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_listen_sockets ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + port integer DEFAULT 8001 NOT NULL, + bind_address character varying(1024), + shoutcast_mount character varying(1024), + shoutcast_compat integer, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_loggings; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_loggings ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + access_log character varying(1024) DEFAULT 'access.log'::character varying NOT NULL, + error_log character varying(1024) DEFAULT 'error.log'::character varying NOT NULL, + playlist_log character varying(1024), + log_level integer DEFAULT 3 NOT NULL, + log_archive integer, + log_size integer DEFAULT 10000, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_master_server_relays; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_master_server_relays ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + master_server character varying(1024) NOT NULL, + master_server_port integer DEFAULT 8001 NOT NULL, + master_update_interval integer DEFAULT 120 NOT NULL, + master_username character varying(64) NOT NULL, + master_pass character varying(64) NOT NULL, + relays_on_demand integer DEFAULT 1 NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_mount_templates; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_mount_templates ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + name character varying(256) NOT NULL, + source_username character varying(64), + source_pass character varying(64), + max_listeners integer DEFAULT 4, + max_listener_duration integer DEFAULT 3600, + dump_file character varying(1024), + intro character varying(1024), + fallback_mount character varying(1024), + fallback_override integer DEFAULT 1, + fallback_when_full integer DEFAULT 1, + charset character varying(1024) DEFAULT 'ISO8859-1'::character varying, + is_public integer DEFAULT 0, + stream_name character varying(1024), + stream_description character varying(10000), + stream_url character varying(1024), + genre character varying(256), + bitrate integer, + mime_type character varying(64) DEFAULT 'audio/mpeg'::character varying NOT NULL, + subtype character varying(64), + burst_size integer, + mp3_metadata_interval integer, + hidden integer DEFAULT 1, + on_connect character varying(1024), + on_disconnect character varying(1024), + authentication_id character varying(64) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_mounts; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNLOGGED TABLE icecast_mounts ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + name character varying(1024) NOT NULL, + source_username character varying(64), + source_pass character varying(64), + max_listeners integer DEFAULT 4, + max_listener_duration integer DEFAULT 3600, + dump_file character varying(1024), + intro character varying(1024), + fallback_mount character varying(1024), + fallback_override integer DEFAULT 1, + fallback_when_full integer DEFAULT 1, + charset character varying(1024) DEFAULT 'ISO8859-1'::character varying, + is_public integer DEFAULT 0, + stream_name character varying(1024), + stream_description character varying(10000), + stream_url character varying(1024), + genre character varying(256), + bitrate integer, + mime_type character varying(64), + subtype character varying(64), + burst_size integer, + mp3_metadata_interval integer, + hidden integer DEFAULT 1, + on_connect character varying(1024), + on_disconnect character varying(1024), + authentication_id character varying(64) DEFAULT NULL::character varying, + listeners integer DEFAULT 0 NOT NULL, + sourced boolean DEFAULT false NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + music_session_id character varying(64), + icecast_server_id character varying(64) NOT NULL, + icecast_mount_template_id character varying(64), + sourced_needs_changing_at timestamp without time zone +); + + +-- +-- Name: icecast_paths; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_paths ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + base_dir character varying(1024) DEFAULT './'::character varying NOT NULL, + log_dir character varying(1024) DEFAULT './logs'::character varying NOT NULL, + pid_file character varying(1024) DEFAULT './icecast.pid'::character varying, + web_root character varying(1024) DEFAULT './web'::character varying NOT NULL, + admin_root character varying(1024) DEFAULT './admin'::character varying NOT NULL, + allow_ip character varying(1024), + deny_ip character varying(1024), + alias_source character varying(1024), + alias_dest character varying(1024), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_relays; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_relays ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + server character varying(1024) NOT NULL, + port integer DEFAULT 8001 NOT NULL, + mount character varying(1024) NOT NULL, + local_mount character varying(1024), + relay_username character varying(64), + relay_pass character varying(64), + relay_shoutcast_metadata integer DEFAULT 0, + on_demand integer DEFAULT 1, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_securities; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_securities ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + chroot integer DEFAULT 0 NOT NULL, + change_owner_user character varying(64), + change_owner_group character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_server_groups; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_server_groups ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + name character varying(255) NOT NULL +); + + +-- +-- Name: icecast_server_mounts; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_server_mounts ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + icecast_mount_id character varying(64), + icecast_server_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_server_relays; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_server_relays ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + icecast_relay_id character varying(64), + icecast_server_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_server_sockets; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_server_sockets ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + icecast_listen_socket_id character varying(64), + icecast_server_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_servers; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_servers ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + config_changed integer DEFAULT 0, + limit_id character varying(64), + admin_auth_id character varying(64), + directory_id character varying(64), + master_relay_id character varying(64), + path_id character varying(64), + logging_id character varying(64), + security_id character varying(64), + template_id character varying(64) NOT NULL, + hostname character varying(1024) NOT NULL, + server_id character varying(1024) NOT NULL, + location character varying(1024), + admin_email character varying(1024), + fileserve integer, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + icecast_server_group_id character varying(64) DEFAULT 'default'::character varying NOT NULL, + mount_template_id character varying(64), + config_updated_at timestamp without time zone +); + + +-- +-- Name: icecast_template_sockets; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_template_sockets ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + icecast_listen_socket_id character varying(64), + icecast_template_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_templates; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE icecast_templates ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + limit_id character varying(64), + admin_auth_id character varying(64), + directory_id character varying(64), + master_relay_id character varying(64), + path_id character varying(64), + logging_id character varying(64), + security_id character varying(64), + location character varying(1024) NOT NULL, + name character varying(256) NOT NULL, + admin_email character varying(1024) DEFAULT 'admin@jamkazam.com'::character varying NOT NULL, + fileserve integer DEFAULT 1 NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: icecast_user_authentications; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNLOGGED TABLE icecast_user_authentications ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + authentication_type character varying(16) DEFAULT 'url'::character varying, + filename character varying(1024), + allow_duplicate_users integer, + mount_add character varying(1024), + mount_remove character varying(1024), + listener_add character varying(1024), + listener_remove character varying(1024), + unused_username character varying(64), + unused_pass character varying(64), + auth_header character varying(64) DEFAULT 'icecast-auth-user: 1'::character varying, + timelimit_header character varying(64) DEFAULT 'icecast-auth-timelimit:'::character varying, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: instruments; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE instruments ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + description character varying(1024) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + popularity integer DEFAULT 0 NOT NULL +); + + +-- +-- Name: invitations; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNLOGGED TABLE invitations ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + sender_id character varying(64), + receiver_id character varying(64), + music_session_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + join_request_id character varying(64) +); + + +-- +-- Name: invited_users; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE invited_users ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + sender_id character varying(64), + autofriend boolean NOT NULL, + email character varying(256), + invitation_code character varying(256) NOT NULL, + accepted boolean DEFAULT false, + note text, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + invite_medium character varying(64) +); + + +-- +-- Name: isp_score_batch; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE isp_score_batch ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + json_scoring_data text NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: jamcompany; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE jamcompany ( + coid integer NOT NULL, + company character varying(50) NOT NULL +); + + +-- +-- Name: jamcompany_coid_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE jamcompany_coid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: jamcompany_coid_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE jamcompany_coid_seq OWNED BY jamcompany.coid; + + +-- +-- Name: jamisp; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE jamisp ( + beginip bigint NOT NULL, + endip bigint NOT NULL, + coid integer NOT NULL, + geom geometry(Polygon) +); + + +-- +-- Name: join_requests; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNLOGGED TABLE join_requests ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64), + music_session_id character varying(64), + text character varying(2000), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: likes; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE likes ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64) NOT NULL, + likable_id character varying(64) NOT NULL, + likable_type character varying(25) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: max_mind_geo; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE max_mind_geo ( + country character varying(2) NOT NULL, + region character varying(2) NOT NULL, + city character varying(255) NOT NULL, + lat numeric(15,10) NOT NULL, + lng numeric(15,10) NOT NULL, + ip_start bigint NOT NULL, + ip_end bigint NOT NULL +); + + +-- +-- Name: max_mind_isp; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE max_mind_isp ( + ip_bottom bigint NOT NULL, + ip_top bigint NOT NULL, + isp character varying(64) NOT NULL, + country character varying(2) NOT NULL +); + + +-- +-- Name: tracks_next_tracker_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE tracks_next_tracker_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: mixes; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE mixes ( + id bigint DEFAULT nextval('tracks_next_tracker_seq'::regclass) NOT NULL, + recording_id character varying(64) NOT NULL, + mix_server character varying(64) DEFAULT NULL::character varying, + started_at timestamp without time zone, + completed_at timestamp without time zone, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + ogg_md5 character varying(100), + ogg_length integer, + ogg_url character varying(1024), + completed boolean DEFAULT false NOT NULL, + error_count integer DEFAULT 0 NOT NULL, + error_reason text, + error_detail text, + should_retry boolean DEFAULT false NOT NULL, + mp3_md5 character varying(100), + mp3_length integer, + mp3_url character varying(1024), + download_count integer DEFAULT 0 NOT NULL, + last_downloaded_at timestamp without time zone +); + + +-- +-- Name: music_session_perf_data; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE music_session_perf_data ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + music_session_id character varying(64), + client_id character varying(64), + uri character varying(1000), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: music_sessions; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE music_sessions ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + music_session_id character varying(64), + description character varying(8000), + user_id character varying(64) NOT NULL, + band_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + session_removed_at timestamp without time zone DEFAULT now(), + play_count integer DEFAULT 0 NOT NULL, + like_count integer DEFAULT 0 NOT NULL, + fan_access boolean DEFAULT true NOT NULL, + scheduled_start timestamp with time zone, + scheduled_duration interval, + musician_access boolean DEFAULT true NOT NULL, + approval_required boolean DEFAULT false NOT NULL, + fan_chat boolean DEFAULT true NOT NULL, + genre_id character varying(64) NOT NULL, + legal_policy character varying(255) DEFAULT 'standard'::character varying NOT NULL, + language character varying(255) DEFAULT 'en'::character varying NOT NULL, + name text NOT NULL, + recurring_session_id character varying(64) +); + + +-- +-- Name: music_sessions_comments; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE music_sessions_comments ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + creator_id character varying(64) NOT NULL, + comment character varying(4000) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + ip_address inet, + music_session_id character varying(64) +); + + +-- +-- Name: music_sessions_likers; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE music_sessions_likers ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + liker_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + ip_address inet, + music_session_id character varying(64) +); + + +-- +-- Name: music_sessions_user_history; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE music_sessions_user_history ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64) NOT NULL, + client_id character varying(64) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + session_removed_at timestamp without time zone, + max_concurrent_connections integer, + rating integer, + instruments character varying(255), + rating_comment text, + music_session_id character varying(64) +); + + +-- +-- Name: musicians_instruments; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE musicians_instruments ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64) NOT NULL, + instrument_id character varying(64) NOT NULL, + proficiency_level smallint NOT NULL, + priority smallint DEFAULT 1 NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: notifications; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE notifications ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + description character varying(32) NOT NULL, + source_user_id character varying(64), + target_user_id character varying(64), + band_id character varying(64), + session_id character varying(64), + recording_id character varying(64), + invitation_id character varying(64), + join_request_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + friend_request_id character varying(64), + band_invitation_id character varying(64), + message text +); + + +-- +-- Name: playable_plays; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE playable_plays ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + playable_id character varying(64) NOT NULL, + playable_type character varying(128) NOT NULL, + player_id character varying(64), + claimed_recording_id character varying(64), + ip_address inet, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: promotionals; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE promotionals ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + type character varying(128) DEFAULT 'JamRuby::PromoBuzz'::character varying NOT NULL, + aasm_state character varying(64) DEFAULT 'hidden'::character varying, + "position" integer DEFAULT 0 NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + latest_id character varying(64) DEFAULT NULL::character varying, + latest_type character varying(128) DEFAULT NULL::character varying, + image character varying(1024) DEFAULT NULL::character varying, + text_short character varying(512) DEFAULT NULL::character varying, + text_long character varying(4096) DEFAULT NULL::character varying +); + + +-- +-- Name: recorded_tracks; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE recorded_tracks ( + id bigint DEFAULT nextval('tracks_next_tracker_seq'::regclass) NOT NULL, + user_id character varying(64) NOT NULL, + instrument_id character varying(64) NOT NULL, + sound character varying(64) NOT NULL, + next_part_to_upload integer DEFAULT 0 NOT NULL, + fully_uploaded boolean DEFAULT false NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + upload_id character varying(1024), + recording_id character varying(64) NOT NULL, + md5 character varying(100), + length bigint, + client_id character varying(64) NOT NULL, + track_id character varying(64) NOT NULL, + url character varying(1024), + file_offset bigint DEFAULT 0, + client_track_id character varying(64) NOT NULL, + is_part_uploading boolean DEFAULT false NOT NULL, + upload_failures integer DEFAULT 0 NOT NULL, + part_failures integer DEFAULT 0 NOT NULL, + discard boolean, + download_count integer DEFAULT 0 NOT NULL, + last_downloaded_at timestamp without time zone +); + + +-- +-- Name: recordings; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE recordings ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + owner_id character varying(64) NOT NULL, + music_session_id character varying(64), + band_id character varying(64), + duration integer, + is_done boolean DEFAULT false, + all_discarded boolean DEFAULT false NOT NULL, + name character varying(1024), + play_count integer DEFAULT 0 NOT NULL, + like_count integer DEFAULT 0 NOT NULL +); + + +-- +-- Name: recordings_comments; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE recordings_comments ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + recording_id character varying(64) NOT NULL, + creator_id character varying(64) NOT NULL, + comment character varying(4000) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + ip_address inet +); + + +-- +-- Name: recordings_downloads; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE recordings_downloads ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + recording_id character varying(64) NOT NULL, + downloader_id character varying(64) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: recordings_likers; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE recordings_likers ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + recording_id character varying(64) NOT NULL, + liker_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + ip_address inet, + claimed_recording_id character varying(64) NOT NULL, + favorite boolean DEFAULT true NOT NULL +); + + +-- +-- Name: recurring_sessions; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE recurring_sessions ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + description character varying(8000), + scheduled_start timestamp with time zone, + scheduled_duration interval, + musician_access boolean NOT NULL, + approval_required boolean NOT NULL, + fan_chat boolean NOT NULL, + genre_id character varying(64), + legal_policy character varying(255) DEFAULT 'standard'::character varying NOT NULL, + language character varying(255) DEFAULT 'en'::character varying NOT NULL, + name text, + user_id character varying(64) NOT NULL, + band_id character varying(64), + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: regions; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE regions ( + region character varying(2) NOT NULL, + regionname character varying(64), + countrycode character varying(2) NOT NULL +); + + +-- +-- Name: rsvp_requests; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE rsvp_requests ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64) NOT NULL, + rsvp_slot_id character varying(64) NOT NULL, + message text, + chosen boolean DEFAULT false, + canceled boolean DEFAULT false, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: rsvp_slots; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE rsvp_slots ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + instrument_id character varying(64), + proficiency_level character varying(255) NOT NULL, + music_session_id character varying(64) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: share_tokens; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE share_tokens ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + token character varying(15) NOT NULL, + shareable_id character varying(64) NOT NULL, + shareable_type character varying(50) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL +); + + +-- +-- Name: tracks; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNLOGGED TABLE tracks ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + connection_id character varying(64) NOT NULL, + instrument_id character varying(64), + sound character varying(64) NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + client_track_id character varying(64) NOT NULL +); + + +-- +-- Name: user_authorizations; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE user_authorizations ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + user_id character varying(64), + uid character varying(255) NOT NULL, + provider character varying(255) NOT NULL, + token character varying(255), + token_expiration timestamp without time zone, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + secret character varying(255) +); + + +-- +-- Name: users; Type: TABLE; Schema: public; Owner: -; Tablespace: +-- + +CREATE TABLE users ( + id character varying(64) DEFAULT uuid_generate_v4() NOT NULL, + email character varying(255) NOT NULL, + remember_token character varying(255), + encrypted_password character varying(255) NOT NULL, + admin boolean DEFAULT false NOT NULL, + created_at timestamp without time zone DEFAULT now() NOT NULL, + updated_at timestamp without time zone DEFAULT now() NOT NULL, + musician boolean DEFAULT false NOT NULL, + city character varying(100), + state character varying(100), + country character varying(100), + first_name character varying(50), + last_name character varying(50), + birth_date date, + gender character(1), + internet_service_provider character varying(50), + signup_token character varying(255), + email_confirmed boolean DEFAULT false, + photo_url character varying(2048), + session_settings character varying(4000), + reset_password_token character varying(64), + reset_password_token_created timestamp without time zone, + can_invite boolean DEFAULT true NOT NULL, + name_tsv tsvector, + environment character varying(255) DEFAULT 'public'::character varying NOT NULL, + subscribe_email boolean DEFAULT true, + update_email character varying(1024), + update_email_token character varying(1024), + original_fpfile character varying(8000) DEFAULT NULL::character varying, + cropped_fpfile character varying(8000) DEFAULT NULL::character varying, + cropped_s3_path character varying(512) DEFAULT NULL::character varying, + crop_selection character varying(256) DEFAULT NULL::character varying, + last_failed_certified_gear_at timestamp without time zone, + last_failed_certified_gear_reason character varying(256), + first_downloaded_client_at timestamp without time zone, + first_ran_client_at timestamp without time zone, + first_certified_gear_at timestamp without time zone, + first_music_session_at timestamp without time zone, + first_real_music_session_at timestamp without time zone, + first_good_music_session_at timestamp without time zone, + first_invited_at timestamp without time zone, + first_friended_at timestamp without time zone, + first_social_promoted_at timestamp without time zone, + show_whats_next boolean DEFAULT true, + biography text, + lat numeric(15,10), + lng numeric(15,10), + icecast_server_group_id character varying(64) DEFAULT 'default'::character varying NOT NULL, + first_recording_at timestamp without time zone, + large_photo_url character varying(2048), + cropped_large_s3_path character varying(512), + cropped_large_fpfile character varying(8000), + addr bigint DEFAULT 0 NOT NULL, + locidispid bigint DEFAULT 0 NOT NULL, + notification_seen_at timestamp without time zone, + affiliate_referral_id character varying(64), + mods json, + audio_latency double precision, + last_jam_addr bigint, + last_jam_locidispid bigint, + last_jam_updated_reason character(1), + last_jam_updated_at timestamp without time zone +); + + +SET search_path = pgmigrate, pg_catalog; + +-- +-- Name: id; Type: DEFAULT; Schema: pgmigrate; Owner: - +-- + +ALTER TABLE ONLY pg_migrate ALTER COLUMN id SET DEFAULT nextval('pg_migrate_id_seq'::regclass); + + +SET search_path = public, pg_catalog; + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY active_admin_comments ALTER COLUMN id SET DEFAULT nextval('active_admin_comments_id_seq'::regclass); + + +-- +-- Name: id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY feeds ALTER COLUMN id SET DEFAULT nextval('feeds_id_seq'::regclass); + + +-- +-- Name: coid; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY jamcompany ALTER COLUMN coid SET DEFAULT nextval('jamcompany_coid_seq'::regclass); + + +SET search_path = pgmigrate, pg_catalog; + +-- +-- Name: pg_migrate_pkey; Type: CONSTRAINT; Schema: pgmigrate; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY pg_migrate + ADD CONSTRAINT pg_migrate_pkey PRIMARY KEY (id); + + +-- +-- Name: pg_migrations_pkey; Type: CONSTRAINT; Schema: pgmigrate; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY pg_migrations + ADD CONSTRAINT pg_migrations_pkey PRIMARY KEY (name); + + +SET search_path = public, pg_catalog; + +-- +-- Name: admin_comments_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY active_admin_comments + ADD CONSTRAINT admin_comments_pkey PRIMARY KEY (id); + + +-- +-- Name: affiliate_partners_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY affiliate_partners + ADD CONSTRAINT affiliate_partners_pkey PRIMARY KEY (id); + + +-- +-- Name: artifact_updates_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY artifact_updates + ADD CONSTRAINT artifact_updates_uniqkey UNIQUE (product, version); + + +-- +-- Name: band_genre_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY bands_genres + ADD CONSTRAINT band_genre_uniqkey UNIQUE (band_id, genre_id); + + +-- +-- Name: band_invitations_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY band_invitations + ADD CONSTRAINT band_invitations_pkey PRIMARY KEY (id); + + +-- +-- Name: band_musician_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY bands_musicians + ADD CONSTRAINT band_musician_uniqkey UNIQUE (band_id, user_id); + + +-- +-- Name: bands_genres_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY bands_genres + ADD CONSTRAINT bands_genres_pkey PRIMARY KEY (id); + + +-- +-- Name: bands_musicians_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY bands_musicians + ADD CONSTRAINT bands_musicians_pkey PRIMARY KEY (id); + + +-- +-- Name: bands_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY bands + ADD CONSTRAINT bands_pkey PRIMARY KEY (id); + + +-- +-- Name: claimed_recordings_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY claimed_recordings + ADD CONSTRAINT claimed_recordings_pkey PRIMARY KEY (id); + + +-- +-- Name: connections_client_id_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY connections + ADD CONSTRAINT connections_client_id_key UNIQUE (client_id); + + +-- +-- Name: connections_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY connections + ADD CONSTRAINT connections_pkey PRIMARY KEY (id); + + +-- +-- Name: crash_dumps_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY crash_dumps + ADD CONSTRAINT crash_dumps_pkey PRIMARY KEY (id); + + +-- +-- Name: email_batch_set_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY email_batch_sets + ADD CONSTRAINT email_batch_set_uniqkey UNIQUE (email_batch_id, started_at); + + +-- +-- Name: email_batch_sets_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY email_batch_sets + ADD CONSTRAINT email_batch_sets_pkey PRIMARY KEY (id); + + +-- +-- Name: email_batches_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY email_batches + ADD CONSTRAINT email_batches_pkey PRIMARY KEY (id); + + +-- +-- Name: email_errors_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY email_errors + ADD CONSTRAINT email_errors_pkey PRIMARY KEY (id); + + +-- +-- Name: event_sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY event_sessions + ADD CONSTRAINT event_sessions_pkey PRIMARY KEY (id); + + +-- +-- Name: events_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY events + ADD CONSTRAINT events_pkey PRIMARY KEY (id); + + +-- +-- Name: events_slug_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY events + ADD CONSTRAINT events_slug_key UNIQUE (slug); + + +-- +-- Name: facebook_signups_lookup_id_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY facebook_signups + ADD CONSTRAINT facebook_signups_lookup_id_key UNIQUE (lookup_id); + + +-- +-- Name: facebook_signups_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY facebook_signups + ADD CONSTRAINT facebook_signups_pkey PRIMARY KEY (id); + + +-- +-- Name: fan_invitations_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY fan_invitations + ADD CONSTRAINT fan_invitations_pkey PRIMARY KEY (id); + + +-- +-- Name: feeds_music_session_id_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY feeds + ADD CONSTRAINT feeds_music_session_id_key UNIQUE (music_session_id); + + +-- +-- Name: feeds_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY feeds + ADD CONSTRAINT feeds_pkey PRIMARY KEY (id); + + +-- +-- Name: feeds_recording_id_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY feeds + ADD CONSTRAINT feeds_recording_id_key UNIQUE (recording_id); + + +-- +-- Name: follows_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY follows + ADD CONSTRAINT follows_pkey PRIMARY KEY (id); + + +-- +-- Name: follows_user_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY follows + ADD CONSTRAINT follows_user_uniqkey UNIQUE (user_id, followable_id); + + +-- +-- Name: friend_requests_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY friend_requests + ADD CONSTRAINT friend_requests_pkey PRIMARY KEY (id); + + +-- +-- Name: friendships_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY friendships + ADD CONSTRAINT friendships_pkey PRIMARY KEY (id); + + +-- +-- Name: genres_music_sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY genres_music_sessions + ADD CONSTRAINT genres_music_sessions_pkey PRIMARY KEY (id); + + +-- +-- Name: genres_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY genres + ADD CONSTRAINT genres_pkey PRIMARY KEY (id); + + +-- +-- Name: geoiplocations_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY geoiplocations + ADD CONSTRAINT geoiplocations_pkey PRIMARY KEY (locid); + + +-- +-- Name: icecast_admin_authentications_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_admin_authentications + ADD CONSTRAINT icecast_admin_authentications_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_directories_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_directories + ADD CONSTRAINT icecast_directories_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_limits_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_limits + ADD CONSTRAINT icecast_limits_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_listen_sockets_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_listen_sockets + ADD CONSTRAINT icecast_listen_sockets_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_loggings_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_loggings + ADD CONSTRAINT icecast_loggings_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_master_server_relays_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_master_server_relays + ADD CONSTRAINT icecast_master_server_relays_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_mount_templates_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_mount_templates + ADD CONSTRAINT icecast_mount_templates_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_mounts_name_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_mounts + ADD CONSTRAINT icecast_mounts_name_key UNIQUE (name); + + +-- +-- Name: icecast_mounts_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_mounts + ADD CONSTRAINT icecast_mounts_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_paths_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_paths + ADD CONSTRAINT icecast_paths_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_relays_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_relays + ADD CONSTRAINT icecast_relays_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_securities_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_securities + ADD CONSTRAINT icecast_securities_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_server_groups_name_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_server_groups + ADD CONSTRAINT icecast_server_groups_name_key UNIQUE (name); + + +-- +-- Name: icecast_server_groups_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_server_groups + ADD CONSTRAINT icecast_server_groups_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_server_mounts_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_server_mounts + ADD CONSTRAINT icecast_server_mounts_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_server_relays_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_server_relays + ADD CONSTRAINT icecast_server_relays_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_server_sockets_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_server_sockets + ADD CONSTRAINT icecast_server_sockets_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_servers_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_servers + ADD CONSTRAINT icecast_servers_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_servers_server_id_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_servers + ADD CONSTRAINT icecast_servers_server_id_key UNIQUE (server_id); + + +-- +-- Name: icecast_template_sockets_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_template_sockets + ADD CONSTRAINT icecast_template_sockets_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_templates_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_templates + ADD CONSTRAINT icecast_templates_pkey PRIMARY KEY (id); + + +-- +-- Name: icecast_user_authentications_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_user_authentications + ADD CONSTRAINT icecast_user_authentications_pkey PRIMARY KEY (id); + + +-- +-- Name: instruments_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY instruments + ADD CONSTRAINT instruments_pkey PRIMARY KEY (id); + + +-- +-- Name: invitations_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY invitations + ADD CONSTRAINT invitations_pkey PRIMARY KEY (id); + + +-- +-- Name: invitations_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY invitations + ADD CONSTRAINT invitations_uniqkey UNIQUE (sender_id, receiver_id, music_session_id); + + +-- +-- Name: invited_users_invitation_code_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY invited_users + ADD CONSTRAINT invited_users_invitation_code_key UNIQUE (invitation_code); + + +-- +-- Name: invited_users_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY invited_users + ADD CONSTRAINT invited_users_pkey PRIMARY KEY (id); + + +-- +-- Name: isp_score_batch_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY isp_score_batch + ADD CONSTRAINT isp_score_batch_pkey PRIMARY KEY (id); + + +-- +-- Name: jamcompany_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY jamcompany + ADD CONSTRAINT jamcompany_pkey PRIMARY KEY (coid); + + +-- +-- Name: join_requests_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY join_requests + ADD CONSTRAINT join_requests_pkey PRIMARY KEY (id); + + +-- +-- Name: likes_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY likes + ADD CONSTRAINT likes_pkey PRIMARY KEY (id); + + +-- +-- Name: likes_user_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY likes + ADD CONSTRAINT likes_user_uniqkey UNIQUE (user_id, likable_id); + + +-- +-- Name: mixes_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY mixes + ADD CONSTRAINT mixes_pkey PRIMARY KEY (id); + + +-- +-- Name: music_session_perf_data_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY music_session_perf_data + ADD CONSTRAINT music_session_perf_data_pkey PRIMARY KEY (id); + + +-- +-- Name: music_session_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY music_sessions + ADD CONSTRAINT music_session_uniqkey UNIQUE (music_session_id); + + +-- +-- Name: music_sessions_comments_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY music_sessions_comments + ADD CONSTRAINT music_sessions_comments_pkey PRIMARY KEY (id); + + +-- +-- Name: music_sessions_history_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY music_sessions + ADD CONSTRAINT music_sessions_history_pkey PRIMARY KEY (id); + + +-- +-- Name: music_sessions_likers_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY music_sessions_likers + ADD CONSTRAINT music_sessions_likers_pkey PRIMARY KEY (id); + + +-- +-- Name: music_sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY active_music_sessions + ADD CONSTRAINT music_sessions_pkey PRIMARY KEY (id); + + +-- +-- Name: musician_instrument_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY musicians_instruments + ADD CONSTRAINT musician_instrument_uniqkey UNIQUE (user_id, instrument_id); + + +-- +-- Name: musician_recording_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY claimed_recordings + ADD CONSTRAINT musician_recording_uniqkey UNIQUE (user_id, recording_id); + + +-- +-- Name: musicians_instruments_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY musicians_instruments + ADD CONSTRAINT musicians_instruments_pkey PRIMARY KEY (id); + + +-- +-- Name: notifications_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY notifications + ADD CONSTRAINT notifications_pkey PRIMARY KEY (id); + + +-- +-- Name: playable_plays_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY playable_plays + ADD CONSTRAINT playable_plays_pkey PRIMARY KEY (id); + + +-- +-- Name: promotionals_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY promotionals + ADD CONSTRAINT promotionals_pkey PRIMARY KEY (id); + + +-- +-- Name: recording_liker_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY recordings_likers + ADD CONSTRAINT recording_liker_uniqkey UNIQUE (recording_id, liker_id); + + +-- +-- Name: recordings_comments_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY recordings_comments + ADD CONSTRAINT recordings_comments_pkey PRIMARY KEY (id); + + +-- +-- Name: recordings_downloads_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY recordings_downloads + ADD CONSTRAINT recordings_downloads_pkey PRIMARY KEY (id); + + +-- +-- Name: recordings_likers_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY recordings_likers + ADD CONSTRAINT recordings_likers_pkey PRIMARY KEY (id); + + +-- +-- Name: recordings_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY recordings + ADD CONSTRAINT recordings_pkey PRIMARY KEY (id); + + +-- +-- Name: recurring_sessions_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY recurring_sessions + ADD CONSTRAINT recurring_sessions_pkey PRIMARY KEY (id); + + +-- +-- Name: rsvp_requests_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY rsvp_requests + ADD CONSTRAINT rsvp_requests_pkey PRIMARY KEY (id); + + +-- +-- Name: rsvp_slots_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY rsvp_slots + ADD CONSTRAINT rsvp_slots_pkey PRIMARY KEY (id); + + +-- +-- Name: saved_tracks_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY recorded_tracks + ADD CONSTRAINT saved_tracks_pkey PRIMARY KEY (id); + + +-- +-- Name: server_mount_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_server_mounts + ADD CONSTRAINT server_mount_uniqkey UNIQUE (icecast_mount_id, icecast_server_id); + + +-- +-- Name: server_relay_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_server_relays + ADD CONSTRAINT server_relay_uniqkey UNIQUE (icecast_relay_id, icecast_server_id); + + +-- +-- Name: server_socket_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_server_sockets + ADD CONSTRAINT server_socket_uniqkey UNIQUE (icecast_listen_socket_id, icecast_server_id); + + +-- +-- Name: share_tokens_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY share_tokens + ADD CONSTRAINT share_tokens_pkey PRIMARY KEY (id); + + +-- +-- Name: template_socket_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY icecast_template_sockets + ADD CONSTRAINT template_socket_uniqkey UNIQUE (icecast_listen_socket_id, icecast_template_id); + + +-- +-- Name: token_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY share_tokens + ADD CONSTRAINT token_uniqkey UNIQUE (token); + + +-- +-- Name: tracks_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY tracks + ADD CONSTRAINT tracks_pkey PRIMARY KEY (id); + + +-- +-- Name: user_authorizations_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY user_authorizations + ADD CONSTRAINT user_authorizations_pkey PRIMARY KEY (id); + + +-- +-- Name: user_authorizations_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY user_authorizations + ADD CONSTRAINT user_authorizations_uniqkey UNIQUE (provider, uid); + + +-- +-- Name: user_friend_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY friendships + ADD CONSTRAINT user_friend_uniqkey UNIQUE (user_id, friend_id); + + +-- +-- Name: user_music_session_uniqkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY join_requests + ADD CONSTRAINT user_music_session_uniqkey UNIQUE (user_id, music_session_id); + + +-- +-- Name: users_email_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY users + ADD CONSTRAINT users_email_key UNIQUE (email); + + +-- +-- Name: users_pkey; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY users + ADD CONSTRAINT users_pkey PRIMARY KEY (id); + + +-- +-- Name: users_remember_token_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY users + ADD CONSTRAINT users_remember_token_key UNIQUE (remember_token); + + +-- +-- Name: users_signup_token_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY users + ADD CONSTRAINT users_signup_token_key UNIQUE (signup_token); + + +-- +-- Name: users_update_email_token_key; Type: CONSTRAINT; Schema: public; Owner: -; Tablespace: +-- + +ALTER TABLE ONLY users + ADD CONSTRAINT users_update_email_token_key UNIQUE (update_email_token); + + +SET search_path = pgmigrate, pg_catalog; + +-- +-- Name: pg_migrate_unique_index; Type: INDEX; Schema: pgmigrate; Owner: -; Tablespace: +-- + +CREATE INDEX pg_migrate_unique_index ON pg_migrate USING btree (template_version, builder_version, migrator_version, database_version); + + +SET search_path = public, pg_catalog; + +-- +-- Name: affiliate_partners_code_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX affiliate_partners_code_idx ON affiliate_partners USING btree (partner_code); + + +-- +-- Name: affiliate_partners_user_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX affiliate_partners_user_idx ON affiliate_partners USING btree (partner_user_id); + + +-- +-- Name: bands_name_tsv_index; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX bands_name_tsv_index ON bands USING gin (name_tsv); + + +-- +-- Name: claimed_recordings_description_tsv_index; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX claimed_recordings_description_tsv_index ON claimed_recordings USING gin (description_tsv); + + +-- +-- Name: claimed_recordings_name_tsv_index; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX claimed_recordings_name_tsv_index ON claimed_recordings USING gin (name_tsv); + + +-- +-- Name: connections_locidispid_ndx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX connections_locidispid_ndx ON connections USING btree (locidispid); + + +-- +-- Name: crash_dumps_client_id_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX crash_dumps_client_id_idx ON crash_dumps USING btree (client_id); + + +-- +-- Name: crash_dumps_timestamp_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX crash_dumps_timestamp_idx ON crash_dumps USING btree ("timestamp"); + + +-- +-- Name: crash_dumps_user_id_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX crash_dumps_user_id_idx ON crash_dumps USING btree (user_id); + + +-- +-- Name: diagnostics_type_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX diagnostics_type_idx ON diagnostics USING btree (type); + + +-- +-- Name: email_batch_set_fkidx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX email_batch_set_fkidx ON email_batch_sets USING btree (email_batch_id); + + +-- +-- Name: email_batch_sets_progress_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX email_batch_sets_progress_idx ON email_batch_sets USING btree (user_id, sub_type); + + +-- +-- Name: email_error_address_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX email_error_address_idx ON email_errors USING btree (email_address); + + +-- +-- Name: email_error_user_fkidx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX email_error_user_fkidx ON email_errors USING btree (user_id); + + +-- +-- Name: geoipblocks_geom_gix; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX geoipblocks_geom_gix ON geoipblocks USING gist (geom); + + +-- +-- Name: geoipisp_company_ndx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX geoipisp_company_ndx ON geoipisp USING btree (company); + + +-- +-- Name: geoiplocations_geog_gix; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX geoiplocations_geog_gix ON geoiplocations USING gist (geog); + + +-- +-- Name: index_active_admin_comments_on_author_type_and_author_id; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_active_admin_comments_on_author_type_and_author_id ON active_admin_comments USING btree (author_type, author_id); + + +-- +-- Name: index_active_admin_comments_on_namespace; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_active_admin_comments_on_namespace ON active_admin_comments USING btree (namespace); + + +-- +-- Name: index_admin_comments_on_resource_type_and_resource_id; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_admin_comments_on_resource_type_and_resource_id ON active_admin_comments USING btree (resource_type, resource_id); + + +-- +-- Name: index_completed_at; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_completed_at ON mixes USING btree (completed_at); + + +-- +-- Name: index_started_at; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX index_started_at ON mixes USING btree (started_at); + + +-- +-- Name: jamcompany_company_ndx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE UNIQUE INDEX jamcompany_company_ndx ON jamcompany USING btree (company); + + +-- +-- Name: jamisp_coid_ndx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX jamisp_coid_ndx ON jamisp USING btree (coid); + + +-- +-- Name: jamisp_geom_gix; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX jamisp_geom_gix ON jamisp USING gist (geom); + + +-- +-- Name: max_mind_isp_ip_bottom_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX max_mind_isp_ip_bottom_idx ON max_mind_isp USING btree (ip_bottom); + + +-- +-- Name: max_mind_isp_ip_top_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX max_mind_isp_ip_top_idx ON max_mind_isp USING btree (ip_top); + + +-- +-- Name: promo_latest_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX promo_latest_idx ON promotionals USING btree (latest_id, latest_type); + + +-- +-- Name: remember_token_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX remember_token_idx ON users USING btree (remember_token); + + +-- +-- Name: scores_alocidispid_blocidispid_score_dt_ndx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX scores_alocidispid_blocidispid_score_dt_ndx ON scores USING btree (alocidispid, blocidispid, score_dt); + + +-- +-- Name: scores_blocidispid_alocidispid_score_dt_ndx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX scores_blocidispid_alocidispid_score_dt_ndx ON scores USING btree (blocidispid, alocidispid, score_dt); + + +-- +-- Name: user_authorizations_user_id_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX user_authorizations_user_id_idx ON user_authorizations USING btree (user_id); + + +-- +-- Name: users_musician_email_idx; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX users_musician_email_idx ON users USING btree (subscribe_email, musician); + + +-- +-- Name: users_name_tsv_index; Type: INDEX; Schema: public; Owner: -; Tablespace: +-- + +CREATE INDEX users_name_tsv_index ON users USING gin (name_tsv); + + +-- +-- Name: tsvectorupdate; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON users FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger('name_tsv', 'public.jamenglish', 'first_name', 'last_name'); + + +-- +-- Name: tsvectorupdate; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE ON bands FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger('name_tsv', 'public.jamenglish', 'name'); + + +-- +-- Name: tsvectorupdate_description; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER tsvectorupdate_description BEFORE INSERT OR UPDATE ON claimed_recordings FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger('description_tsv', 'public.jamenglish', 'description'); + + +-- +-- Name: tsvectorupdate_name; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER tsvectorupdate_name BEFORE INSERT OR UPDATE ON claimed_recordings FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger('name_tsv', 'public.jamenglish', 'name'); + + +SET search_path = pgmigrate, pg_catalog; + +-- +-- Name: pg_migrations_pg_migrate_id_fkey; Type: FK CONSTRAINT; Schema: pgmigrate; Owner: - +-- + +ALTER TABLE ONLY pg_migrations + ADD CONSTRAINT pg_migrations_pg_migrate_id_fkey FOREIGN KEY (pg_migrate_id) REFERENCES pg_migrate(id); + + +SET search_path = public, pg_catalog; + +-- +-- Name: band_invitations_band_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY band_invitations + ADD CONSTRAINT band_invitations_band_id_fkey FOREIGN KEY (band_id) REFERENCES bands(id) ON DELETE CASCADE; + + +-- +-- Name: band_invitations_creator_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY band_invitations + ADD CONSTRAINT band_invitations_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: band_invitations_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY band_invitations + ADD CONSTRAINT band_invitations_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: bands_genres_band_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY bands_genres + ADD CONSTRAINT bands_genres_band_id_fkey FOREIGN KEY (band_id) REFERENCES bands(id) ON DELETE CASCADE; + + +-- +-- Name: bands_genres_genre_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY bands_genres + ADD CONSTRAINT bands_genres_genre_id_fkey FOREIGN KEY (genre_id) REFERENCES genres(id) ON DELETE CASCADE; + + +-- +-- Name: bands_musicians_band_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY bands_musicians + ADD CONSTRAINT bands_musicians_band_id_fkey FOREIGN KEY (band_id) REFERENCES bands(id) ON DELETE CASCADE; + + +-- +-- Name: bands_musicians_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY bands_musicians + ADD CONSTRAINT bands_musicians_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: chat_messages_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY chat_messages + ADD CONSTRAINT chat_messages_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: claimed_recordings_genre_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY claimed_recordings + ADD CONSTRAINT claimed_recordings_genre_id_fkey FOREIGN KEY (genre_id) REFERENCES genres(id); + + +-- +-- Name: connections_music_session_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY connections + ADD CONSTRAINT connections_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES active_music_sessions(id) ON DELETE SET NULL; + + +-- +-- Name: connections_tracks_connection_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY tracks + ADD CONSTRAINT connections_tracks_connection_id_fkey FOREIGN KEY (connection_id) REFERENCES connections(id) ON DELETE CASCADE; + + +-- +-- Name: crash_dumps_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY crash_dumps + ADD CONSTRAINT crash_dumps_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; + + +-- +-- Name: diagnostics_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY diagnostics + ADD CONSTRAINT diagnostics_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: email_batch_sets_email_batch_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY email_batch_sets + ADD CONSTRAINT email_batch_sets_email_batch_id_fkey FOREIGN KEY (email_batch_id) REFERENCES email_batches(id) ON DELETE CASCADE; + + +-- +-- Name: email_errors_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY email_errors + ADD CONSTRAINT email_errors_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: event_sessions_band_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY event_sessions + ADD CONSTRAINT event_sessions_band_id_fkey FOREIGN KEY (band_id) REFERENCES bands(id) ON DELETE SET NULL; + + +-- +-- Name: event_sessions_event_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY event_sessions + ADD CONSTRAINT event_sessions_event_id_fkey FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE; + + +-- +-- Name: event_sessions_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY event_sessions + ADD CONSTRAINT event_sessions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL; + + +-- +-- Name: fan_invitations_music_session_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY fan_invitations + ADD CONSTRAINT fan_invitations_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + + +-- +-- Name: feeds_music_session_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY feeds + ADD CONSTRAINT feeds_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + + +-- +-- Name: feeds_recording_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY feeds + ADD CONSTRAINT feeds_recording_id_fkey FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE; + + +-- +-- Name: follows_user_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY follows + ADD CONSTRAINT follows_user_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: friend_requests_friend_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY friend_requests + ADD CONSTRAINT friend_requests_friend_id_fkey FOREIGN KEY (friend_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: friend_requests_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY friend_requests + ADD CONSTRAINT friend_requests_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: friendships_friend_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY friendships + ADD CONSTRAINT friendships_friend_id_fkey FOREIGN KEY (friend_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: friendships_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY friendships + ADD CONSTRAINT friendships_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: genres_music_sessions_music_session_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY genres_music_sessions + ADD CONSTRAINT genres_music_sessions_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES active_music_sessions(id) ON DELETE CASCADE; + + +-- +-- Name: icecast_mounts_icecast_mount_template_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_mounts + ADD CONSTRAINT icecast_mounts_icecast_mount_template_id_fkey FOREIGN KEY (icecast_mount_template_id) REFERENCES icecast_mount_templates(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_mounts_icecast_server_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_mounts + ADD CONSTRAINT icecast_mounts_icecast_server_id_fkey FOREIGN KEY (icecast_server_id) REFERENCES icecast_servers(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_mounts_music_session_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_mounts + ADD CONSTRAINT icecast_mounts_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES active_music_sessions(id) ON DELETE CASCADE; + + +-- +-- Name: icecast_server_mounts_icecast_server_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_server_mounts + ADD CONSTRAINT icecast_server_mounts_icecast_server_id_fkey FOREIGN KEY (icecast_server_id) REFERENCES icecast_servers(id) ON DELETE CASCADE; + + +-- +-- Name: icecast_server_relays_icecast_relay_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_server_relays + ADD CONSTRAINT icecast_server_relays_icecast_relay_id_fkey FOREIGN KEY (icecast_relay_id) REFERENCES icecast_relays(id) ON DELETE CASCADE; + + +-- +-- Name: icecast_server_relays_icecast_server_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_server_relays + ADD CONSTRAINT icecast_server_relays_icecast_server_id_fkey FOREIGN KEY (icecast_server_id) REFERENCES icecast_servers(id) ON DELETE CASCADE; + + +-- +-- Name: icecast_server_sockets_icecast_listen_socket_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_server_sockets + ADD CONSTRAINT icecast_server_sockets_icecast_listen_socket_id_fkey FOREIGN KEY (icecast_listen_socket_id) REFERENCES icecast_listen_sockets(id) ON DELETE CASCADE; + + +-- +-- Name: icecast_server_sockets_icecast_server_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_server_sockets + ADD CONSTRAINT icecast_server_sockets_icecast_server_id_fkey FOREIGN KEY (icecast_server_id) REFERENCES icecast_servers(id) ON DELETE CASCADE; + + +-- +-- Name: icecast_servers_admin_auth_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_servers + ADD CONSTRAINT icecast_servers_admin_auth_id_fkey FOREIGN KEY (admin_auth_id) REFERENCES icecast_admin_authentications(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_servers_directory_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_servers + ADD CONSTRAINT icecast_servers_directory_id_fkey FOREIGN KEY (directory_id) REFERENCES icecast_directories(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_servers_icecast_server_group_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_servers + ADD CONSTRAINT icecast_servers_icecast_server_group_id_fkey FOREIGN KEY (icecast_server_group_id) REFERENCES icecast_server_groups(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_servers_limit_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_servers + ADD CONSTRAINT icecast_servers_limit_id_fkey FOREIGN KEY (limit_id) REFERENCES icecast_limits(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_servers_logging_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_servers + ADD CONSTRAINT icecast_servers_logging_id_fkey FOREIGN KEY (logging_id) REFERENCES icecast_loggings(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_servers_master_relay_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_servers + ADD CONSTRAINT icecast_servers_master_relay_id_fkey FOREIGN KEY (master_relay_id) REFERENCES icecast_master_server_relays(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_servers_mount_template_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_servers + ADD CONSTRAINT icecast_servers_mount_template_id_fkey FOREIGN KEY (mount_template_id) REFERENCES icecast_mount_templates(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_servers_path_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_servers + ADD CONSTRAINT icecast_servers_path_id_fkey FOREIGN KEY (path_id) REFERENCES icecast_paths(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_servers_security_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_servers + ADD CONSTRAINT icecast_servers_security_id_fkey FOREIGN KEY (security_id) REFERENCES icecast_securities(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_servers_template_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_servers + ADD CONSTRAINT icecast_servers_template_id_fkey FOREIGN KEY (template_id) REFERENCES icecast_templates(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_template_sockets_icecast_listen_socket_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_template_sockets + ADD CONSTRAINT icecast_template_sockets_icecast_listen_socket_id_fkey FOREIGN KEY (icecast_listen_socket_id) REFERENCES icecast_listen_sockets(id) ON DELETE CASCADE; + + +-- +-- Name: icecast_template_sockets_icecast_template_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_template_sockets + ADD CONSTRAINT icecast_template_sockets_icecast_template_id_fkey FOREIGN KEY (icecast_template_id) REFERENCES icecast_templates(id) ON DELETE CASCADE; + + +-- +-- Name: icecast_templates_admin_auth_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_templates + ADD CONSTRAINT icecast_templates_admin_auth_id_fkey FOREIGN KEY (admin_auth_id) REFERENCES icecast_admin_authentications(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_templates_directory_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_templates + ADD CONSTRAINT icecast_templates_directory_id_fkey FOREIGN KEY (directory_id) REFERENCES icecast_directories(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_templates_limit_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_templates + ADD CONSTRAINT icecast_templates_limit_id_fkey FOREIGN KEY (limit_id) REFERENCES icecast_limits(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_templates_logging_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_templates + ADD CONSTRAINT icecast_templates_logging_id_fkey FOREIGN KEY (logging_id) REFERENCES icecast_loggings(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_templates_master_relay_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_templates + ADD CONSTRAINT icecast_templates_master_relay_id_fkey FOREIGN KEY (master_relay_id) REFERENCES icecast_master_server_relays(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_templates_path_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_templates + ADD CONSTRAINT icecast_templates_path_id_fkey FOREIGN KEY (path_id) REFERENCES icecast_paths(id) ON DELETE SET NULL; + + +-- +-- Name: icecast_templates_security_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY icecast_templates + ADD CONSTRAINT icecast_templates_security_id_fkey FOREIGN KEY (security_id) REFERENCES icecast_securities(id) ON DELETE SET NULL; + + +-- +-- Name: invitations_join_request_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY invitations + ADD CONSTRAINT invitations_join_request_id_fkey FOREIGN KEY (join_request_id) REFERENCES join_requests(id) ON DELETE CASCADE; + + +-- +-- Name: invitations_music_session_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY invitations + ADD CONSTRAINT invitations_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + + +-- +-- Name: invited_users_sender_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY invited_users + ADD CONSTRAINT invited_users_sender_id_fkey FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: join_requests_music_session_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY join_requests + ADD CONSTRAINT join_requests_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + + +-- +-- Name: likes_user_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY likes + ADD CONSTRAINT likes_user_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: mixes_recording_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY mixes + ADD CONSTRAINT mixes_recording_id_fkey FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE; + + +-- +-- Name: music_sessions_comments_creator_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY music_sessions_comments + ADD CONSTRAINT music_sessions_comments_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: music_sessions_comments_music_session_id2_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY music_sessions_comments + ADD CONSTRAINT music_sessions_comments_music_session_id2_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + + +-- +-- Name: music_sessions_history_band_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY music_sessions + ADD CONSTRAINT music_sessions_history_band_id_fkey FOREIGN KEY (band_id) REFERENCES bands(id) ON DELETE CASCADE; + + +-- +-- Name: music_sessions_history_genre_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY music_sessions + ADD CONSTRAINT music_sessions_history_genre_id_fkey FOREIGN KEY (genre_id) REFERENCES genres(id); + + +-- +-- Name: music_sessions_history_recurring_session_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY music_sessions + ADD CONSTRAINT music_sessions_history_recurring_session_id_fkey FOREIGN KEY (recurring_session_id) REFERENCES recurring_sessions(id); + + +-- +-- Name: music_sessions_history_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY music_sessions + ADD CONSTRAINT music_sessions_history_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: music_sessions_likers_liker_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY music_sessions_likers + ADD CONSTRAINT music_sessions_likers_liker_id_fkey FOREIGN KEY (liker_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: music_sessions_likers_music_session_id2_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY music_sessions_likers + ADD CONSTRAINT music_sessions_likers_music_session_id2_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + + +-- +-- Name: music_sessions_user_history_music_session_id2_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY music_sessions_user_history + ADD CONSTRAINT music_sessions_user_history_music_session_id2_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + + +-- +-- Name: music_sessions_user_history_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY music_sessions_user_history + ADD CONSTRAINT music_sessions_user_history_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: musicians_instruments_instrument_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY musicians_instruments + ADD CONSTRAINT musicians_instruments_instrument_id_fkey FOREIGN KEY (instrument_id) REFERENCES instruments(id) ON DELETE CASCADE; + + +-- +-- Name: musicians_instruments_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY musicians_instruments + ADD CONSTRAINT musicians_instruments_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: musicians_recordings_recording_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY claimed_recordings + ADD CONSTRAINT musicians_recordings_recording_id_fkey FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE; + + +-- +-- Name: musicians_recordings_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY claimed_recordings + ADD CONSTRAINT musicians_recordings_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: notifications_band_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY notifications + ADD CONSTRAINT notifications_band_id_fkey FOREIGN KEY (band_id) REFERENCES bands(id) ON DELETE CASCADE; + + +-- +-- Name: notifications_band_invitation_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY notifications + ADD CONSTRAINT notifications_band_invitation_id_fkey FOREIGN KEY (band_invitation_id) REFERENCES band_invitations(id) ON DELETE CASCADE; + + +-- +-- Name: notifications_friend_request_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY notifications + ADD CONSTRAINT notifications_friend_request_id_fkey FOREIGN KEY (friend_request_id) REFERENCES friend_requests(id); + + +-- +-- Name: notifications_recording_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY notifications + ADD CONSTRAINT notifications_recording_id_fkey FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE; + + +-- +-- Name: notifications_source_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY notifications + ADD CONSTRAINT notifications_source_user_id_fkey FOREIGN KEY (source_user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: notifications_target_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY notifications + ADD CONSTRAINT notifications_target_user_id_fkey FOREIGN KEY (target_user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: playable_plays_claimed_recording_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY playable_plays + ADD CONSTRAINT playable_plays_claimed_recording_id_fkey FOREIGN KEY (claimed_recording_id) REFERENCES claimed_recordings(id) ON DELETE CASCADE; + + +-- +-- Name: playable_plays_player_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY playable_plays + ADD CONSTRAINT playable_plays_player_id_fkey FOREIGN KEY (player_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: recorded_tracks_recording_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recorded_tracks + ADD CONSTRAINT recorded_tracks_recording_id_fkey FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE; + + +-- +-- Name: recordings_band_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recordings + ADD CONSTRAINT recordings_band_id_fkey FOREIGN KEY (band_id) REFERENCES bands(id); + + +-- +-- Name: recordings_comments_creator_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recordings_comments + ADD CONSTRAINT recordings_comments_creator_id_fkey FOREIGN KEY (creator_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: recordings_comments_recording_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recordings_comments + ADD CONSTRAINT recordings_comments_recording_id_fkey FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE; + + +-- +-- Name: recordings_creator_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recordings + ADD CONSTRAINT recordings_creator_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: recordings_downloads_downloader_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recordings_downloads + ADD CONSTRAINT recordings_downloads_downloader_id_fkey FOREIGN KEY (downloader_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: recordings_downloads_recording_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recordings_downloads + ADD CONSTRAINT recordings_downloads_recording_id_fkey FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE; + + +-- +-- Name: recordings_likers_claimed_recording_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recordings_likers + ADD CONSTRAINT recordings_likers_claimed_recording_id_fkey FOREIGN KEY (claimed_recording_id) REFERENCES claimed_recordings(id); + + +-- +-- Name: recordings_likers_liker_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recordings_likers + ADD CONSTRAINT recordings_likers_liker_id_fkey FOREIGN KEY (liker_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: recordings_likers_recording_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recordings_likers + ADD CONSTRAINT recordings_likers_recording_id_fkey FOREIGN KEY (recording_id) REFERENCES recordings(id) ON DELETE CASCADE; + + +-- +-- Name: recurring_sessions_band_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recurring_sessions + ADD CONSTRAINT recurring_sessions_band_id_fkey FOREIGN KEY (band_id) REFERENCES bands(id) ON DELETE CASCADE; + + +-- +-- Name: recurring_sessions_genre_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recurring_sessions + ADD CONSTRAINT recurring_sessions_genre_id_fkey FOREIGN KEY (genre_id) REFERENCES genres(id); + + +-- +-- Name: recurring_sessions_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recurring_sessions + ADD CONSTRAINT recurring_sessions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: rsvp_requests_rsvp_slot_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY rsvp_requests + ADD CONSTRAINT rsvp_requests_rsvp_slot_id_fkey FOREIGN KEY (rsvp_slot_id) REFERENCES rsvp_slots(id) ON DELETE CASCADE; + + +-- +-- Name: rsvp_requests_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY rsvp_requests + ADD CONSTRAINT rsvp_requests_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: rsvp_slots_instrument_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY rsvp_slots + ADD CONSTRAINT rsvp_slots_instrument_id_fkey FOREIGN KEY (instrument_id) REFERENCES instruments(id); + + +-- +-- Name: rsvp_slots_music_session_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY rsvp_slots + ADD CONSTRAINT rsvp_slots_music_session_id_fkey FOREIGN KEY (music_session_id) REFERENCES music_sessions(id) ON DELETE CASCADE; + + +-- +-- Name: saved_tracks_instrument_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recorded_tracks + ADD CONSTRAINT saved_tracks_instrument_id_fkey FOREIGN KEY (instrument_id) REFERENCES instruments(id) ON DELETE CASCADE; + + +-- +-- Name: saved_tracks_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY recorded_tracks + ADD CONSTRAINT saved_tracks_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: user_authorizations_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY user_authorizations + ADD CONSTRAINT user_authorizations_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + + +-- +-- Name: users_affiliate_referral_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY users + ADD CONSTRAINT users_affiliate_referral_id_fkey FOREIGN KEY (affiliate_referral_id) REFERENCES affiliate_partners(id); + + +-- +-- Name: users_icecast_server_group_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY users + ADD CONSTRAINT users_icecast_server_group_id_fkey FOREIGN KEY (icecast_server_group_id) REFERENCES icecast_server_groups(id); + + +-- +-- PostgreSQL database dump complete +-- + +SET search_path TO "$user", public, topology; + diff --git a/web/lib/tasks/email.rake b/web/lib/tasks/email.rake new file mode 100644 index 000000000..0d2a77977 --- /dev/null +++ b/web/lib/tasks/email.rake @@ -0,0 +1,13 @@ +namespace :emails do + + task :test_progression_email => :environment do |task, args| + user = User.find_by_email('jonathan@jamkazam.com') + EmailBatchProgression::SUBTYPES.each do |stype| + ebatch = EmailBatchProgression.create + ebatch.update_attribute(:sub_type, stype) + ebatch.email_batch_sets << (bset = ebatch.make_set(user, 0)) + ProgressMailer.send_reminder(bset).deliver + end + end + +end diff --git a/web/lib/tasks/google.rake b/web/lib/tasks/google.rake new file mode 100644 index 000000000..c3ef000eb --- /dev/null +++ b/web/lib/tasks/google.rake @@ -0,0 +1,13 @@ +=begin +require 'google/api_client' + +namespace :google do + + task :youtube do |task, args| + client = Google::APIClient.new + yt = client.discovered_api('youtube', 'v3') + # google-api oauth-2-login --client-id='785931784279-gd0g8on6sc0tuesj7cu763pitaiv2la8.apps.googleusercontent.com' --client-secret='UwzIcvtErv9c2-GIsNfIo7bA' --scope="https://www.googleapis.com/auth/plus.me" + end + +end +=end \ No newline at end of file diff --git a/web/lib/tasks/sample_data.rake b/web/lib/tasks/sample_data.rake index 88c496ddf..8ba62eb5a 100644 --- a/web/lib/tasks/sample_data.rake +++ b/web/lib/tasks/sample_data.rake @@ -88,6 +88,17 @@ namespace :db do make_music_sessions_history make_music_sessions_user_history end + + task affiliate_traffic_earnings: :environment do + partner = FactoryGirl.create(:affiliate_partner) + user_partner = FactoryGirl.create(:user, affiliate_partner: partner) + + puts "USER CREATED: u: #{user_partner.email}/p: foobar" + + today = Date.today + quarter1 = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner, due_amount_in_cents: 10000, closed:true) + month1 = FactoryGirl.create(:affiliate_monthly_payment, affiliate_partner: partner, due_amount_in_cents: 10000, closed:true) + end end def make_music_sessions_history diff --git a/web/lib/user_manager.rb b/web/lib/user_manager.rb index 0ce291b60..8f231a57b 100644 --- a/web/lib/user_manager.rb +++ b/web/lib/user_manager.rb @@ -28,6 +28,7 @@ class UserManager < BaseManager affiliate_referral_id = options[:affiliate_referral_id] any_user = options[:any_user] signup_hint = options[:signup_hint] + affiliate_partner = options[:affiliate_partner] recaptcha_failed = false unless options[:skip_recaptcha] # allow callers to opt-of recaptcha @@ -70,7 +71,8 @@ class UserManager < BaseManager signup_confirm_url: signup_confirm_url, affiliate_referral_id: affiliate_referral_id, any_user: any_user, - signup_hint: signup_hint) + signup_hint: signup_hint, + affiliate_partner: affiliate_partner) user end diff --git a/web/loop.bash b/web/loop.bash new file mode 100755 index 000000000..1c38b6d12 --- /dev/null +++ b/web/loop.bash @@ -0,0 +1,7 @@ +#!/bin/bash + +for i in {1..100} +do + echo "Loop $i "`date` + bundle exec rspec spec/features/music_sessions_spec.rb +done \ No newline at end of file diff --git a/web/public/maintenance.html b/web/public/maintenance.html index d0641191d..2edb104cc 100644 --- a/web/public/maintenance.html +++ b/web/public/maintenance.html @@ -82,7 +82,7 @@ table { border-collapse: collapse; border-spacing: 0px; } caption, th, td { text-align: left; font-weight: normal; vertical-align: middle; } a img { border: medium none; } - body { color: white; background-color: rgb(38, 38, 38); width: 100%; height: 100%; overflow: hidden; font-size: 14px; font-family: Raleway,Arial,Helvetica,sans-serif; font-weight: 300; } + body { color: white; background-color: rgb(38, 38, 38); width: 100%; height: 100%; overflow: hidden; font-size: 14px; font-family: 'Raleway',Arial,Helvetica,sans-serif; font-weight: 300; } a { cursor: pointer; color: rgb(255, 204, 0); text-decoration: none; display: inline-block; } a:hover { text-decoration: underline; color: rgb(130, 174, 175); } select { padding: 3px; font-size: 15px; border-radius: 6px; } diff --git a/web/ruby-bt.txt b/web/ruby-bt.txt new file mode 100644 index 000000000..e5351db3a --- /dev/null +++ b/web/ruby-bt.txt @@ -0,0 +1,919 @@ +$ bundle exec rake routes +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/common.rb:67: [BUG] Segmentation fault +ruby 2.0.0p247 (2013-06-27 revision 41674) [x86_64-linux] + +-- Control frame information ----------------------------------------------- +c:0056 p:---- s:0192 e:000191 CFUNC :initialize +c:0055 p:---- s:0190 e:000189 CFUNC :new +c:0054 p:0075 s:0187 e:000184 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/common.rb:67 +c:0053 p:0070 s:0176 e:000174 CLASS /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext.rb:17 +c:0052 p:0011 s:0173 e:000172 CLASS /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext.rb:12 +c:0051 p:0057 s:0171 e:000170 TOP /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext.rb:9 [FINISH] +c:0050 p:---- s:0169 e:000168 CFUNC :require +c:0049 p:0010 s:0165 e:000164 BLOCK /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251 +c:0048 p:0054 s:0163 e:000162 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:236 +c:0047 p:0015 s:0158 e:000157 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251 +c:0046 p:0019 s:0153 e:000152 CLASS /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json.rb:58 +c:0045 p:0017 s:0151 e:000150 TOP /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json.rb:54 [FINISH] +c:0044 p:---- s:0149 e:000148 CFUNC :require +c:0043 p:0010 s:0145 e:000144 BLOCK /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251 +c:0042 p:0054 s:0143 e:000142 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:236 +c:0041 p:0015 s:0138 e:000137 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251 +c:0040 p:0009 s:0133 e:000132 TOP /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/object/to_json.rb:3 [FINISH] +c:0039 p:---- s:0131 e:000130 CFUNC :require +c:0038 p:0010 s:0127 e:000126 BLOCK /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251 +c:0037 p:0054 s:0125 e:000124 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:236 +c:0036 p:0015 s:0120 e:000119 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251 +c:0035 p:0007 s:0115 e:000114 TOP /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/json/encoding.rb:1 [FINISH] +c:0034 p:---- s:0113 e:000112 CFUNC :require +c:0033 p:0010 s:0109 e:000108 BLOCK /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251 +c:0032 p:0054 s:0107 e:000106 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:236 +c:0031 p:0015 s:0102 e:000101 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251 +c:0030 p:0015 s:0097 e:000096 TOP /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/json.rb:2 [FINISH] +c:0029 p:---- s:0095 e:000094 CFUNC :require +c:0028 p:0010 s:0091 e:000090 BLOCK /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251 +c:0027 p:0054 s:0089 e:000088 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:236 +c:0026 p:0015 s:0084 e:000083 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251 +c:0025 p:0007 s:0079 e:000078 TOP /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/serializers/json.rb:1 [FINISH] +c:0024 p:0029 s:0077 e:000075 CLASS /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/serialization.rb:5 +c:0023 p:0011 s:0074 e:000073 CLASS /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/serialization.rb:3 +c:0022 p:0009 s:0072 e:000071 TOP /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/serialization.rb:1 [FINISH] +c:0021 p:0770 s:0070 e:000065 CLASS /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/base.rb:715 +c:0020 p:0011 s:0064 e:000063 CLASS /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/base.rb:333 +c:0019 p:0228 s:0062 e:000061 TOP /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/base.rb:33 [FINISH] +c:0018 p:0068 s:0060 e:000059 TOP /home/jam/workspace/jam-cloud/web/config/application.rb:13 [FINISH] +c:0017 p:---- s:0058 e:000057 CFUNC :require +c:0016 p:0018 s:0054 e:000053 TOP /home/jam/workspace/jam-cloud/web/Rakefile:5 [FINISH] +c:0015 p:---- s:0052 e:000051 CFUNC :load +c:0014 p:0009 s:0048 e:000047 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/rake_module.rb:25 +c:0013 p:0176 s:0044 e:000043 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb:637 +c:0012 p:0007 s:0039 e:000038 BLOCK /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb:94 +c:0011 p:0006 s:0037 e:000036 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb:165 +c:0010 p:0007 s:0033 e:000032 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb:93 +c:0009 p:0013 s:0030 e:000029 BLOCK /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb:77 +c:0008 p:0006 s:0028 e:000027 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb:165 +c:0007 p:0007 s:0024 e:000023 METHOD /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb:75 +c:0006 p:0040 s:0021 e:000020 TOP /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/bin/rake:33 [FINISH] +c:0005 p:---- s:0019 e:000018 CFUNC :load +c:0004 p:0118 s:0015 E:000da8 EVAL /home/jam/.rvm/gems/ruby-2.0.0-p247@global/bin/rake:23 [FINISH] +c:0003 p:---- s:0011 e:000010 CFUNC :eval +c:0002 p:0118 s:0005 E:001d40 EVAL /home/jam/.rvm/gems/ruby-2.0.0-p247@global/bin/ruby_executable_hooks:15 [FINISH] +c:0001 p:0000 s:0002 E:000338 TOP [FINISH] + +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/bin/ruby_executable_hooks:15:in `
    ' +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/bin/ruby_executable_hooks:15:in `eval' +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/bin/rake:23:in `
    ' +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/bin/rake:23:in `load' +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/bin/rake:33:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb:75:in `run' +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb:165:in `standard_exception_handling' +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb:77:in `block in run' +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb:93:in `load_rakefile' +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb:165:in `standard_exception_handling' +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb:94:in `block in load_rakefile' +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb:637:in `raw_load_rakefile' +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/rake_module.rb:25:in `load_rakefile' +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/rake_module.rb:25:in `load' +/home/jam/workspace/jam-cloud/web/Rakefile:5:in `' +/home/jam/workspace/jam-cloud/web/Rakefile:5:in `require' +/home/jam/workspace/jam-cloud/web/config/application.rb:13:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/base.rb:33:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/base.rb:333:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/base.rb:715:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/serialization.rb:1:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/serialization.rb:3:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/serialization.rb:5:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/serializers/json.rb:1:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:236:in `load_dependency' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `block in require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/json.rb:2:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:236:in `load_dependency' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `block in require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/json/encoding.rb:1:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:236:in `load_dependency' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `block in require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/object/to_json.rb:3:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:236:in `load_dependency' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `block in require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json.rb:54:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json.rb:58:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:236:in `load_dependency' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `block in require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb:251:in `require' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext.rb:9:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext.rb:12:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext.rb:17:in `' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/common.rb:67:in `generator=' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/common.rb:67:in `new' +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/common.rb:67:in `initialize' + +-- C level backtrace information ------------------------------------------- +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x19d7f5) [0x7fd2a8a937f5] vm_dump.c:647 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x64dbc) [0x7fd2a895adbc] error.c:283 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_bug+0xb7) [0x7fd2a895c2c7] error.c:302 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x129cee) [0x7fd2a8a1fcee] signal.c:672 +/lib/x86_64-linux-gnu/libc.so.6(+0x364a0) [0x7fd2a856d4a0] +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_hash_aref+0x11) [0x7fd2a897ce31] hash.c:561 +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext/generator.so(+0x3294) [0x7fd2a3f99294] generator.c:528 +/home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext/generator.so(+0x4d9f) [0x7fd2a3f9ad9f] generator.c:954 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x193440) [0x7fd2a8a89440] vm_eval.c:117 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x1939e3) [0x7fd2a8a899e3] vm_eval.c:49 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_class_new_instance+0x30) [0x7fd2a89b5ae0] object.c:1761 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x187c06) [0x7fd2a8a7dc06] vm_insnhelper.c:1469 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x1965ff) [0x7fd2a8a8c5ff] vm_insnhelper.c:1559 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x18c2d2) [0x7fd2a8a822d2] insns.def:1017 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x192036) [0x7fd2a8a88036] vm.c:1201 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_iseq_eval+0x176) [0x7fd2a8a8e8f6] vm.c:1436 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x6f784) [0x7fd2a8965784] load.c:599 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_require_safe+0x4ff) [0x7fd2a896757f] load.c:959 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x187c06) [0x7fd2a8a7dc06] vm_insnhelper.c:1469 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x1965ff) [0x7fd2a8a8c5ff] vm_insnhelper.c:1559 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x18c45d) [0x7fd2a8a8245d] insns.def:1039 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x192036) [0x7fd2a8a88036] vm.c:1201 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_iseq_eval+0x176) [0x7fd2a8a8e8f6] vm.c:1436 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x6f784) [0x7fd2a8965784] load.c:599 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_require_safe+0x4ff) [0x7fd2a896757f] load.c:959 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x187c06) [0x7fd2a8a7dc06] vm_insnhelper.c:1469 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x1965ff) [0x7fd2a8a8c5ff] vm_insnhelper.c:1559 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x18c45d) [0x7fd2a8a8245d] insns.def:1039 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x192036) [0x7fd2a8a88036] vm.c:1201 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_iseq_eval+0x176) [0x7fd2a8a8e8f6] vm.c:1436 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x6f784) [0x7fd2a8965784] load.c:599 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_require_safe+0x4ff) [0x7fd2a896757f] load.c:959 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x187c06) [0x7fd2a8a7dc06] vm_insnhelper.c:1469 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x1965ff) [0x7fd2a8a8c5ff] vm_insnhelper.c:1559 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x18c45d) [0x7fd2a8a8245d] insns.def:1039 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x192036) [0x7fd2a8a88036] vm.c:1201 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_iseq_eval+0x176) [0x7fd2a8a8e8f6] vm.c:1436 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x6f784) [0x7fd2a8965784] load.c:599 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_require_safe+0x4ff) [0x7fd2a896757f] load.c:959 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x187c06) [0x7fd2a8a7dc06] vm_insnhelper.c:1469 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x1965ff) [0x7fd2a8a8c5ff] vm_insnhelper.c:1559 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x18c45d) [0x7fd2a8a8245d] insns.def:1039 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x192036) [0x7fd2a8a88036] vm.c:1201 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_iseq_eval+0x176) [0x7fd2a8a8e8f6] vm.c:1436 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x6f784) [0x7fd2a8965784] load.c:599 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_require_safe+0x4ff) [0x7fd2a896757f] load.c:959 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x187c06) [0x7fd2a8a7dc06] vm_insnhelper.c:1469 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x1965ff) [0x7fd2a8a8c5ff] vm_insnhelper.c:1559 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x18c45d) [0x7fd2a8a8245d] insns.def:1039 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x192036) [0x7fd2a8a88036] vm.c:1201 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_iseq_eval+0x176) [0x7fd2a8a8e8f6] vm.c:1436 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x6f784) [0x7fd2a8965784] load.c:599 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_require_safe+0x4ff) [0x7fd2a896757f] load.c:959 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_protect+0xd7) [0x7fd2a8963307] eval.c:786 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_autoload_load+0x10c) [0x7fd2a8a614fc] variable.c:1781 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x16b687) [0x7fd2a8a61687] variable.c:1841 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x189e48) [0x7fd2a8a7fe48] vm_insnhelper.c:450 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x192036) [0x7fd2a8a88036] vm.c:1201 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_iseq_eval+0x176) [0x7fd2a8a8e8f6] vm.c:1436 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x6f784) [0x7fd2a8965784] load.c:599 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_require_safe+0x4ff) [0x7fd2a896757f] load.c:959 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_protect+0xd7) [0x7fd2a8963307] eval.c:786 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_autoload_load+0x10c) [0x7fd2a8a614fc] variable.c:1781 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x18784f) [0x7fd2a8a7d84f] vm_insnhelper.c:414 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x18d92f) [0x7fd2a8a8392f] vm_insnhelper.c:383 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x192036) [0x7fd2a8a88036] vm.c:1201 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_iseq_eval+0x176) [0x7fd2a8a8e8f6] vm.c:1436 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x6f784) [0x7fd2a8965784] load.c:599 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_require_safe+0x4ff) [0x7fd2a896757f] load.c:959 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_protect+0xd7) [0x7fd2a8963307] eval.c:786 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_autoload_load+0x10c) [0x7fd2a8a614fc] variable.c:1781 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x16b687) [0x7fd2a8a61687] variable.c:1841 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x189e48) [0x7fd2a8a7fe48] vm_insnhelper.c:450 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x192036) [0x7fd2a8a88036] vm.c:1201 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_iseq_eval+0x176) [0x7fd2a8a8e8f6] vm.c:1436 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x6f784) [0x7fd2a8965784] load.c:599 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_require_safe+0x4ff) [0x7fd2a896757f] load.c:959 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x187c06) [0x7fd2a8a7dc06] vm_insnhelper.c:1469 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x1965ff) [0x7fd2a8a8c5ff] vm_insnhelper.c:1559 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x18c2d2) [0x7fd2a8a822d2] insns.def:1017 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x192036) [0x7fd2a8a88036] vm.c:1201 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_iseq_eval+0x176) [0x7fd2a8a8e8f6] vm.c:1436 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x6f784) [0x7fd2a8965784] load.c:599 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x6f8e8) [0x7fd2a89658e8] load.c:680 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x187c06) [0x7fd2a8a7dc06] vm_insnhelper.c:1469 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x1965ff) [0x7fd2a8a8c5ff] vm_insnhelper.c:1559 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x18c2d2) [0x7fd2a8a822d2] insns.def:1017 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x192036) [0x7fd2a8a88036] vm.c:1201 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_iseq_eval+0x176) [0x7fd2a8a8e8f6] vm.c:1436 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x6f784) [0x7fd2a8965784] load.c:599 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x6f8e8) [0x7fd2a89658e8] load.c:680 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x187c06) [0x7fd2a8a7dc06] vm_insnhelper.c:1469 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x1965ff) [0x7fd2a8a8c5ff] vm_insnhelper.c:1559 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x18c2d2) [0x7fd2a8a822d2] insns.def:1017 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x192036) [0x7fd2a8a88036] vm.c:1201 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x19259c) [0x7fd2a8a8859c] vm_eval.c:1251 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x192a1f) [0x7fd2a8a88a1f] vm_eval.c:1292 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x187c06) [0x7fd2a8a7dc06] vm_insnhelper.c:1469 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x1965ff) [0x7fd2a8a8c5ff] vm_insnhelper.c:1559 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x18c2d2) [0x7fd2a8a822d2] insns.def:1017 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x192036) [0x7fd2a8a88036] vm.c:1201 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(rb_iseq_eval_main+0x8a) [0x7fd2a8a8e9da] vm.c:1449 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(+0x6b8c2) [0x7fd2a89618c2] eval.c:250 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(ruby_exec_node+0x1d) [0x7fd2a8962bad] eval.c:315 +/home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/../lib/libruby.so.2.0(ruby_run_node+0x1e) [0x7fd2a8964dae] eval.c:307 +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/bin/rake() [0x40080b] +/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed) [0x7fd2a855876d] +/home/jam/.rvm/gems/ruby-2.0.0-p247@global/bin/rake() [0x400839] + +-- Other runtime information ----------------------------------------------- + +* Loaded script: /home/jam/.rvm/gems/ruby-2.0.0-p247@global/bin/rake + +* Loaded features: + + 0 enumerator.so + 1 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/encdb.so + 2 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/trans/transdb.so + 3 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/rbconfig.rb + 4 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/compatibility.rb + 5 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/defaults.rb + 6 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/deprecate.rb + 7 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/errors.rb + 8 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/version.rb + 9 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/requirement.rb + 10 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/platform.rb + 11 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/specification.rb + 12 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/exceptions.rb + 13 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/core_ext/kernel_gem.rb + 14 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/core_ext/kernel_require.rb + 15 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems.rb + 16 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/pathname.so + 17 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/pathname.rb + 18 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/user_interaction.rb + 19 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/etc.so + 20 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/config_file.rb + 21 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/rubygems_integration.rb + 22 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/dependency.rb + 23 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/shared_helpers.rb + 24 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/fileutils.rb + 25 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/gem_path_manipulation.rb + 26 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/gem_helpers.rb + 27 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/match_platform.rb + 28 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/rubygems_ext.rb + 29 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/version.rb + 30 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler.rb + 31 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/settings.rb + 32 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/path_support.rb + 33 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/digest.so + 34 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/digest.rb + 35 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/digest/sha1.so + 36 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/set.rb + 37 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/definition.rb + 38 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/dependency.rb + 39 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/ruby_dsl.rb + 40 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/dsl.rb + 41 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/source.rb + 42 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/uri/common.rb + 43 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/uri/generic.rb + 44 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/uri/ftp.rb + 45 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/uri/http.rb + 46 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/uri/https.rb + 47 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/uri/ldap.rb + 48 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/uri/ldaps.rb + 49 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/uri/mailto.rb + 50 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/uri.rb + 51 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/socket.so + 52 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/socket.rb + 53 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/fcntl.so + 54 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/timeout.rb + 55 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/thread.rb + 56 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/openssl.so + 57 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/openssl/bn.rb + 58 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/openssl/cipher.rb + 59 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/stringio.so + 60 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/openssl/config.rb + 61 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/openssl/digest.rb + 62 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/openssl/x509.rb + 63 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/openssl/buffering.rb + 64 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/openssl/ssl.rb + 65 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/openssl.rb + 66 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/securerandom.rb + 67 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/resolv.rb + 68 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/remote_fetcher.rb + 69 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/text.rb + 70 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/name_tuple.rb + 71 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/site_ruby/2.0.0/rubygems/spec_fetcher.rb + 72 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/source/rubygems.rb + 73 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/source/path.rb + 74 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/strscan.so + 75 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/source/git.rb + 76 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/lockfile_parser.rb + 77 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/lazy_specification.rb + 78 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/tsort.rb + 79 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/forwardable.rb + 80 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/spec_set.rb + 81 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/index.rb + 82 /home/jam/workspace/jam-cloud/websocket-gateway/lib/jam_websockets/version.rb + 83 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/environment.rb + 84 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/runtime.rb + 85 /home/jam/workspace/jam-cloud/db/target/ruby_package/lib/jam_db/version.rb + 86 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/remote_specification.rb + 87 /home/jam/workspace/jam-cloud/ruby/lib/jam_ruby/version.rb + 88 /home/jam/workspace/jam-cloud/pb/target/ruby/jampb/lib/jampb/version.rb + 89 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/dep_proxy.rb + 90 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/resolver.rb + 91 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/ui.rb + 92 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/endpoint_specification.rb + 93 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/bundler-1.3.5/lib/bundler/setup.rb + 94 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/version.rb + 95 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/singleton.rb + 96 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/monitor.rb + 97 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/optparse.rb + 98 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/ostruct.rb + 99 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/ext/module.rb + 100 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/ext/core.rb + 101 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/ext/string.rb + 102 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/early_time.rb + 103 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/ext/time.rb + 104 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/alt_system.rb + 105 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/win32.rb + 106 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/linked_list.rb + 107 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/scope.rb + 108 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/task_argument_error.rb + 109 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/rule_recursion_overflow_error.rb + 110 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/shellwords.rb + 111 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/task_manager.rb + 112 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/cloneable.rb + 113 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/file_utils.rb + 114 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/file_utils_ext.rb + 115 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/pathmap.rb + 116 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/file_list.rb + 117 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/promise.rb + 118 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/thread_pool.rb + 119 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/private_reader.rb + 120 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/thread_history_display.rb + 121 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/trace_output.rb + 122 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/application.rb + 123 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/rake_module.rb + 124 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/pseudo_status.rb + 125 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/task_arguments.rb + 126 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/invocation_chain.rb + 127 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/invocation_exception_mixin.rb + 128 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/task.rb + 129 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/file_task.rb + 130 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/file_creation_task.rb + 131 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/multi_task.rb + 132 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/dsl_definition.rb + 133 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/default_loader.rb + 134 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/name_space.rb + 135 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake/backtrace.rb + 136 /home/jam/.rvm/gems/ruby-2.0.0-p247@global/gems/rake-10.1.0/lib/rake.rb + 137 /home/jam/workspace/jam-cloud/web/config/boot.rb + 138 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/inflector/inflections.rb + 139 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/inflections.rb + 140 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/inflector/methods.rb + 141 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/lazy_load_hooks.rb + 142 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies/autoload.rb + 143 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/version.rb + 144 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support.rb + 145 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/i18n-0.6.5/lib/i18n/version.rb + 146 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/i18n-0.6.5/lib/i18n/exceptions.rb + 147 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/i18n-0.6.5/lib/i18n/interpolate/ruby.rb + 148 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/i18n-0.6.5/lib/i18n.rb + 149 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/i18n-0.6.5/lib/i18n/config.rb + 150 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/i18n.rb + 151 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/version.rb + 152 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model.rb + 153 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/crud.rb + 154 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/factory_methods.rb + 155 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/expressions.rb + 156 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/predications.rb + 157 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/math.rb + 158 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/alias_predication.rb + 159 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/order_predications.rb + 160 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/table.rb + 161 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/attributes/attribute.rb + 162 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/attributes.rb + 163 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/compatibility/wheres.rb + 164 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/relation.rb + 165 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/expression.rb + 166 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors/visitor.rb + 167 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors/depth_first.rb + 168 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/bigdecimal.so + 169 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/date_core.so + 170 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/date/format.rb + 171 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/date.rb + 172 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors/to_sql.rb + 173 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors/sqlite.rb + 174 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors/postgresql.rb + 175 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors/mysql.rb + 176 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors/mssql.rb + 177 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors/oracle.rb + 178 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors/join_sql.rb + 179 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors/where_sql.rb + 180 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors/order_clauses.rb + 181 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors/dot.rb + 182 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors/ibm_db.rb + 183 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors/informix.rb + 184 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/visitors.rb + 185 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/tree_manager.rb + 186 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/insert_manager.rb + 187 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/select_manager.rb + 188 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/update_manager.rb + 189 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/delete_manager.rb + 190 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/node.rb + 191 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/select_statement.rb + 192 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/select_core.rb + 193 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/insert_statement.rb + 194 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/update_statement.rb + 195 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/terminal.rb + 196 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/true.rb + 197 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/false.rb + 198 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/unary.rb + 199 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/ascending.rb + 200 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/descending.rb + 201 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/unqualified_column.rb + 202 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/with.rb + 203 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/binary.rb + 204 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/equality.rb + 205 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/in.rb + 206 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/join_source.rb + 207 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/delete_statement.rb + 208 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/table_alias.rb + 209 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/infix_operation.rb + 210 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/and.rb + 211 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/function.rb + 212 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/count.rb + 213 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/values.rb + 214 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/named_function.rb + 215 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/inner_join.rb + 216 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/outer_join.rb + 217 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/string_join.rb + 218 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes/sql_literal.rb + 219 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/nodes.rb + 220 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/deprecated.rb + 221 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/sql/engine.rb + 222 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel/sql_literal.rb + 223 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/arel-3.0.2/lib/arel.rb + 224 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/version.rb + 225 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/syntax_error.rb + 226 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/psych.so + 227 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/nodes/node.rb + 228 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/nodes/stream.rb + 229 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/nodes/document.rb + 230 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/nodes/sequence.rb + 231 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/nodes/scalar.rb + 232 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/nodes/mapping.rb + 233 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/nodes/alias.rb + 234 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/nodes.rb + 235 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/streaming.rb + 236 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/visitors/visitor.rb + 237 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/scalar_scanner.rb + 238 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/visitors/to_ruby.rb + 239 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/visitors/emitter.rb + 240 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/visitors/yaml_tree.rb + 241 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/json/ruby_events.rb + 242 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/visitors/json_tree.rb + 243 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/visitors/depth_first.rb + 244 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/visitors.rb + 245 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/handler.rb + 246 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/tree_builder.rb + 247 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/parser.rb + 248 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/omap.rb + 249 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/set.rb + 250 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/coder.rb + 251 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/core_ext.rb + 252 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/deprecated.rb + 253 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/stream.rb + 254 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/json/yaml_events.rb + 255 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/json/tree_builder.rb + 256 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/json/stream.rb + 257 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych/handlers/document_stream.rb + 258 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/psych.rb + 259 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/yaml.rb + 260 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/ordered_hash.rb + 261 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/enumerable.rb + 262 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/notifications/fanout.rb + 263 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/notifications.rb + 264 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/array/wrap.rb + 265 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/deprecation/behaviors.rb + 266 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/deprecation/reporting.rb + 267 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/module/deprecation.rb + 268 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/module/aliasing.rb + 269 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/array/extract_options.rb + 270 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/deprecation/method_wrappers.rb + 271 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/deprecation/proxy_wrappers.rb + 272 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/deprecation.rb + 273 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/concern.rb + 274 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/hash/keys.rb + 275 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/kernel/singleton_class.rb + 276 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/module/remove_method.rb + 277 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/class/attribute.rb + 278 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/attribute_methods.rb + 279 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/attribute_methods.rb + 280 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/bigdecimal/util.rb + 281 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/benchmark.rb + 282 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/benchmark.rb + 283 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/connection_adapters/schema_cache.rb + 284 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/big_decimal/conversions.rb + 285 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/connection_adapters/abstract/quoting.rb + 286 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/connection_adapters/abstract/database_statements.rb + 287 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/connection_adapters/abstract/schema_statements.rb + 288 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/connection_adapters/abstract/database_limits.rb + 289 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/connection_adapters/abstract/query_cache.rb + 290 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/descendants_tracker.rb + 291 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/kernel/reporting.rb + 292 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/object/inclusion.rb + 293 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/callbacks.rb + 294 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/connection_adapters/abstract_adapter.rb + 295 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/scoping.rb + 296 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record.rb + 297 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/railties-3.2.15/lib/rails/ruby_version_check.rb + 298 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/class/attribute_accessors.rb + 299 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/logger.rb + 300 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/logger.rb + 301 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/hash/reverse_merge.rb + 302 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/railties-3.2.15/lib/rails/initializable.rb + 303 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/ordered_options.rb + 304 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/hash/deep_dup.rb + 305 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/railties-3.2.15/lib/rails/paths.rb + 306 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/railties-3.2.15/lib/rails/rack.rb + 307 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/railties-3.2.15/lib/rails/configuration.rb + 308 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/module/attribute_accessors.rb + 309 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/multibyte/utils.rb + 310 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/multibyte.rb + 311 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/string/multibyte.rb + 312 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/inflector/transliterate.rb + 313 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/string/inflections.rb + 314 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/inflector.rb + 315 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/module/introspection.rb + 316 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/module/delegation.rb + 317 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/railties-3.2.15/lib/rails/railtie.rb + 318 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/railties-3.2.15/lib/rails/engine/railties.rb + 319 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/railties-3.2.15/lib/rails/engine.rb + 320 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/time.rb + 321 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/base64.rb + 322 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/base64.rb + 323 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/string/encoding.rb + 324 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/object/blank.rb + 325 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/xml_mini/rexml.rb + 326 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/xml_mini.rb + 327 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/array/conversions.rb + 328 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/railties-3.2.15/lib/rails/plugin.rb + 329 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/railties-3.2.15/lib/rails/application.rb + 330 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/railties-3.2.15/lib/rails/version.rb + 331 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/file_update_checker.rb + 332 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/railties-3.2.15/lib/rails/railtie/configurable.rb + 333 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/railties-3.2.15/lib/rails/railtie/configuration.rb + 334 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/i18n_railtie.rb + 335 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/railtie.rb + 336 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionpack-3.2.15/lib/action_pack/version.rb + 337 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionpack-3.2.15/lib/action_pack.rb + 338 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/rack-1.4.5/lib/rack.rb + 339 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionpack-3.2.15/lib/action_dispatch.rb + 340 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionpack-3.2.15/lib/action_dispatch/railtie.rb + 341 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/railties-3.2.15/lib/rails.rb + 342 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/railtie.rb + 343 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/basic_object.rb + 344 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/object/acts_like.rb + 345 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/duration.rb + 346 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/object/try.rb + 347 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/values/time_zone.rb + 348 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/time_with_zone.rb + 349 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/time/zones.rb + 350 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/date/zones.rb + 351 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/date/calculations.rb + 352 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/time/publicize_conversion_methods.rb + 353 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/time/conversions.rb + 354 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/date_time/calculations.rb + 355 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/date_time/conversions.rb + 356 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/process/daemon.rb + 357 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/time/calculations.rb + 358 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/string/conversions.rb + 359 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/i18n-0.6.5/lib/i18n/core_ext/string/interpolate.rb + 360 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/string/interpolation.rb + 361 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/rexml/rexml.rb + 362 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/rexml.rb + 363 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/file/path.rb + 364 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/module/method_names.rb + 365 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/ruby/shim.rb + 366 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/module/attr_internal.rb + 367 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/module/anonymous.rb + 368 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionpack-3.2.15/lib/abstract_controller.rb + 369 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/cgi/util.rb + 370 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/erb.rb + 371 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/string/output_safety.rb + 372 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionpack-3.2.15/lib/action_view.rb + 373 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionpack-3.2.15/lib/action_controller/vendor/html-scanner.rb + 374 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/load_error.rb + 375 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/name_error.rb + 376 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/uri.rb + 377 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionpack-3.2.15/lib/action_controller.rb + 378 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionpack-3.2.15/lib/action_view/railtie.rb + 379 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionpack-3.2.15/lib/abstract_controller/railties/routes_helpers.rb + 380 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionpack-3.2.15/lib/action_controller/railties/paths.rb + 381 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionpack-3.2.15/lib/action_controller/railtie.rb + 382 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/railtie.rb + 383 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionmailer-3.2.15/lib/action_mailer/version.rb + 384 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/class/delegating_attributes.rb + 385 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/module/reachable.rb + 386 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/class/subclasses.rb + 387 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/class.rb + 388 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/array/uniq_by.rb + 389 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionmailer-3.2.15/lib/action_mailer.rb + 390 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionmailer-3.2.15/lib/action_mailer/railtie.rb + 391 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activeresource-3.2.15/lib/active_resource/exceptions.rb + 392 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activeresource-3.2.15/lib/active_resource/version.rb + 393 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activeresource-3.2.15/lib/active_resource.rb + 394 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activeresource-3.2.15/lib/active_resource/railtie.rb + 395 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/actionpack-3.2.15/lib/sprockets/railtie.rb + 396 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/benchmarkable.rb + 397 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/module/qualified_const.rb + 398 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/string/starts_ends_with.rb + 399 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/dependencies.rb + 400 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/time/marshal.rb + 401 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/time/acts_like.rb + 402 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/date/acts_like.rb + 403 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/date/freeze.rb + 404 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/date/conversions.rb + 405 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/date_time/acts_like.rb + 406 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/date_time/zones.rb + 407 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/integer/time.rb + 408 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/numeric/time.rb + 409 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/time.rb + 410 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/hash/deep_merge.rb + 411 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/hash_with_indifferent_access.rb + 412 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/hash/indifferent_access.rb + 413 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/hash/slice.rb + 414 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/string/behavior.rb + 415 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/object/duplicable.rb + 416 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/errors.rb + 417 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/log_subscriber.rb + 418 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/log_subscriber.rb + 419 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/explain_subscriber.rb + 420 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/persistence.rb + 421 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/hash/except.rb + 422 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/naming.rb + 423 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/query_cache.rb + 424 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/querying.rb + 425 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/readonly_attributes.rb + 426 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/model_schema.rb + 427 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/translation.rb + 428 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/translation.rb + 429 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/inheritance.rb + 430 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/scoping/default.rb + 431 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/array/access.rb + 432 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/array/grouping.rb + 433 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/array/random_access.rb + 434 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/array/prepend_and_append.rb + 435 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/array.rb + 436 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/scoping/named.rb + 437 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/dynamic_matchers.rb + 438 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/sanitization.rb + 439 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/mass_assignment_security/permission_set.rb + 440 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/mass_assignment_security/sanitizer.rb + 441 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/mass_assignment_security.rb + 442 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/attribute_assignment.rb + 443 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/conversion.rb + 444 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/integration.rb + 445 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/errors.rb + 446 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/validations/callbacks.rb + 447 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/validator.rb + 448 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/validations/acceptance.rb + 449 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/validations/confirmation.rb + 450 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/range/blockless_step.rb + 451 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/range/conversions.rb + 452 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/range/include_range.rb + 453 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/range/overlaps.rb + 454 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/range/cover.rb + 455 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/core_ext/range.rb + 456 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/validations/exclusion.rb + 457 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/validations/format.rb + 458 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/validations/inclusion.rb + 459 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/validations/length.rb + 460 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/validations/numericality.rb + 461 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/validations/presence.rb + 462 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/validations/validates.rb + 463 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/validations/with.rb + 464 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/validations.rb + 465 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/validations/associated.rb + 466 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/validations/uniqueness.rb + 467 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/validations.rb + 468 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/counter_cache.rb + 469 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/locking/optimistic.rb + 470 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/locking/pessimistic.rb + 471 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/attribute_methods/read.rb + 472 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/attribute_methods/write.rb + 473 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/attribute_methods/before_type_cast.rb + 474 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/attribute_methods/query.rb + 475 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/attribute_methods/primary_key.rb + 476 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/attribute_methods/time_zone_conversion.rb + 477 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/dirty.rb + 478 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/attribute_methods/dirty.rb + 479 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/timestamp.rb + 480 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/attribute_methods/serialization.rb + 481 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/attribute_methods/deprecated_underscore_read.rb + 482 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/callbacks.rb + 483 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/observer_array.rb + 484 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/observing.rb + 485 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/callbacks.rb + 486 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/associations.rb + 487 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/identity_map.rb + 488 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activemodel-3.2.15/lib/active_model/secure_password.rb + 489 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/explain.rb + 490 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/autosave_association.rb + 491 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/nested_attributes.rb + 492 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/associations/builder/association.rb + 493 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/associations/builder/singular_association.rb + 494 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/associations/builder/has_one.rb + 495 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/associations/builder/collection_association.rb + 496 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/associations/builder/has_many.rb + 497 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/associations/builder/belongs_to.rb + 498 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/associations/builder/has_and_belongs_to_many.rb + 499 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/aggregations.rb + 500 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/transactions.rb + 501 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activerecord-3.2.15/lib/active_record/reflection.rb + 502 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/multi_json-1.8.2/lib/multi_json/options.rb + 503 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/multi_json-1.8.2/lib/multi_json/version.rb + 504 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/multi_json-1.8.2/lib/multi_json/load_error.rb + 505 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/multi_json-1.8.2/lib/multi_json.rb + 506 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/activesupport-3.2.15/lib/active_support/json/decoding.rb + 507 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/version.rb + 508 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/generic_object.rb + 509 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/common.rb + 510 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_16be.so + 511 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_16le.so + 512 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_32be.so + 513 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_32le.so + 514 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext/parser.so + 515 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext/generator.so + +* Process memory map: + +00400000-00401000 r-xp 00000000 fc:00 2230335 /home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/ruby +00600000-00601000 r--p 00000000 fc:00 2230335 /home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/ruby +00601000-00602000 rw-p 00001000 fc:00 2230335 /home/jam/.rvm/rubies/ruby-2.0.0-p247/bin/ruby +00cbb000-02b22000 rw-p 00000000 00:00 0 [heap] +7fd2a3d80000-7fd2a3d95000 r-xp 00000000 fc:00 1572908 /lib/x86_64-linux-gnu/libgcc_s.so.1 +7fd2a3d95000-7fd2a3f94000 ---p 00015000 fc:00 1572908 /lib/x86_64-linux-gnu/libgcc_s.so.1 +7fd2a3f94000-7fd2a3f95000 r--p 00014000 fc:00 1572908 /lib/x86_64-linux-gnu/libgcc_s.so.1 +7fd2a3f95000-7fd2a3f96000 rw-p 00015000 fc:00 1572908 /lib/x86_64-linux-gnu/libgcc_s.so.1 +7fd2a3f96000-7fd2a3f9e000 r-xp 00000000 fc:00 2493168 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext/generator.so +7fd2a3f9e000-7fd2a419d000 ---p 00008000 fc:00 2493168 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext/generator.so +7fd2a419d000-7fd2a419e000 r--p 00007000 fc:00 2493168 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext/generator.so +7fd2a419e000-7fd2a419f000 rw-p 00008000 fc:00 2493168 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext/generator.so +7fd2a419f000-7fd2a41a0000 r-xp 00000000 fc:00 2229480 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_32le.so +7fd2a41a0000-7fd2a439f000 ---p 00001000 fc:00 2229480 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_32le.so +7fd2a439f000-7fd2a43a0000 r--p 00000000 fc:00 2229480 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_32le.so +7fd2a43a0000-7fd2a43a1000 rw-p 00001000 fc:00 2229480 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_32le.so +7fd2a43a1000-7fd2a43a2000 r-xp 00000000 fc:00 2229448 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_32be.so +7fd2a43a2000-7fd2a45a1000 ---p 00001000 fc:00 2229448 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_32be.so +7fd2a45a1000-7fd2a45a2000 r--p 00000000 fc:00 2229448 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_32be.so +7fd2a45a2000-7fd2a45a3000 rw-p 00001000 fc:00 2229448 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_32be.so +7fd2a45a3000-7fd2a45a4000 r-xp 00000000 fc:00 2229439 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_16le.so +7fd2a45a4000-7fd2a47a3000 ---p 00001000 fc:00 2229439 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_16le.so +7fd2a47a3000-7fd2a47a4000 r--p 00000000 fc:00 2229439 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_16le.so +7fd2a47a4000-7fd2a47a5000 rw-p 00001000 fc:00 2229439 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_16le.so +7fd2a47a5000-7fd2a47a6000 r-xp 00000000 fc:00 2229440 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_16be.so +7fd2a47a6000-7fd2a49a5000 ---p 00001000 fc:00 2229440 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_16be.so +7fd2a49a5000-7fd2a49a6000 r--p 00000000 fc:00 2229440 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_16be.so +7fd2a49a6000-7fd2a49a7000 rw-p 00001000 fc:00 2229440 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/utf_16be.so +7fd2a49a7000-7fd2a4b97000 r-xp 00000000 fc:00 938271 /usr/lib/libruby-1.9.1.so.1.9.1 +7fd2a4b97000-7fd2a4d96000 ---p 001f0000 fc:00 938271 /usr/lib/libruby-1.9.1.so.1.9.1 +7fd2a4d96000-7fd2a4d9b000 r--p 001ef000 fc:00 938271 /usr/lib/libruby-1.9.1.so.1.9.1 +7fd2a4d9b000-7fd2a4d9f000 rw-p 001f4000 fc:00 938271 /usr/lib/libruby-1.9.1.so.1.9.1 +7fd2a4d9f000-7fd2a4dbb000 rw-p 00000000 00:00 0 +7fd2a4dbb000-7fd2a4dc1000 r-xp 00000000 fc:00 2493172 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext/parser.so +7fd2a4dc1000-7fd2a4fc0000 ---p 00006000 fc:00 2493172 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext/parser.so +7fd2a4fc0000-7fd2a4fc1000 r--p 00005000 fc:00 2493172 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext/parser.so +7fd2a4fc1000-7fd2a4fc2000 rw-p 00006000 fc:00 2493172 /home/jam/.rvm/gems/ruby-2.0.0-p247/gems/json-1.8.1/lib/json/ext/parser.so +7fd2a4fc2000-7fd2a4fe2000 r-xp 00000000 fc:00 938243 /usr/lib/x86_64-linux-gnu/libyaml-0.so.2.0.2 +7fd2a4fe2000-7fd2a51e1000 ---p 00020000 fc:00 938243 /usr/lib/x86_64-linux-gnu/libyaml-0.so.2.0.2 +7fd2a51e1000-7fd2a51e2000 r--p 0001f000 fc:00 938243 /usr/lib/x86_64-linux-gnu/libyaml-0.so.2.0.2 +7fd2a51e2000-7fd2a51e3000 rw-p 00020000 fc:00 938243 /usr/lib/x86_64-linux-gnu/libyaml-0.so.2.0.2 +7fd2a51e3000-7fd2a51e9000 r-xp 00000000 fc:00 2229434 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/psych.so +7fd2a51e9000-7fd2a53e8000 ---p 00006000 fc:00 2229434 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/psych.so +7fd2a53e8000-7fd2a53e9000 r--p 00005000 fc:00 2229434 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/psych.so +7fd2a53e9000-7fd2a53ea000 rw-p 00006000 fc:00 2229434 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/psych.so +7fd2a53ea000-7fd2a541c000 r-xp 00000000 fc:00 2229497 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/date_core.so +7fd2a541c000-7fd2a561c000 ---p 00032000 fc:00 2229497 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/date_core.so +7fd2a561c000-7fd2a561d000 r--p 00032000 fc:00 2229497 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/date_core.so +7fd2a561d000-7fd2a561e000 rw-p 00033000 fc:00 2229497 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/date_core.so +7fd2a561e000-7fd2a5620000 rw-p 00000000 00:00 0 +7fd2a5620000-7fd2a5634000 r-xp 00000000 fc:00 2229399 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/bigdecimal.so +7fd2a5634000-7fd2a5833000 ---p 00014000 fc:00 2229399 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/bigdecimal.so +7fd2a5833000-7fd2a5834000 r--p 00013000 fc:00 2229399 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/bigdecimal.so +7fd2a5834000-7fd2a5835000 rw-p 00014000 fc:00 2229399 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/bigdecimal.so +7fd2a5835000-7fd2a583a000 r-xp 00000000 fc:00 2229498 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/strscan.so +7fd2a583a000-7fd2a5a39000 ---p 00005000 fc:00 2229498 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/strscan.so +7fd2a5a39000-7fd2a5a3a000 r--p 00004000 fc:00 2229498 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/strscan.so +7fd2a5a3a000-7fd2a5a3b000 rw-p 00005000 fc:00 2229498 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/strscan.so +7fd2a5a3b000-7fd2a5a42000 r-xp 00000000 fc:00 2229401 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/stringio.so +7fd2a5a42000-7fd2a5c41000 ---p 00007000 fc:00 2229401 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/stringio.so +7fd2a5c41000-7fd2a5c42000 r--p 00006000 fc:00 2229401 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/stringio.so +7fd2a5c42000-7fd2a5c43000 rw-p 00007000 fc:00 2229401 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/stringio.so +7fd2a5c43000-7fd2a5c95000 r-xp 00000000 fc:00 1572880 /lib/x86_64-linux-gnu/libssl.so.1.0.0 +7fd2a5c95000-7fd2a5e95000 ---p 00052000 fc:00 1572880 /lib/x86_64-linux-gnu/libssl.so.1.0.0 +7fd2a5e95000-7fd2a5e98000 r--p 00052000 fc:00 1572880 /lib/x86_64-linux-gnu/libssl.so.1.0.0 +7fd2a5e98000-7fd2a5e9e000 rw-p 00055000 fc:00 1572880 /lib/x86_64-linux-gnu/libssl.so.1.0.0 +7fd2a5e9e000-7fd2a5e9f000 rw-p 00000000 00:00 0 +7fd2a5e9f000-7fd2a5eed000 r-xp 00000000 fc:00 2229414 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/openssl.so +7fd2a5eed000-7fd2a60ed000 ---p 0004e000 fc:00 2229414 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/openssl.so +7fd2a60ed000-7fd2a60ee000 r--p 0004e000 fc:00 2229414 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/openssl.so +7fd2a60ee000-7fd2a60f0000 rw-p 0004f000 fc:00 2229414 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/openssl.so +7fd2a60f0000-7fd2a60f1000 rw-p 00000000 00:00 0 +7fd2a60f1000-7fd2a60f2000 r-xp 00000000 fc:00 2229402 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/fcntl.so +7fd2a60f2000-7fd2a62f1000 ---p 00001000 fc:00 2229402 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/fcntl.so +7fd2a62f1000-7fd2a62f2000 r--p 00000000 fc:00 2229402 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/fcntl.so +7fd2a62f2000-7fd2a62f3000 rw-p 00001000 fc:00 2229402 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/fcntl.so +7fd2a62f3000-7fd2a6316000 r-xp 00000000 fc:00 2229409 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/socket.so +7fd2a6316000-7fd2a6516000 ---p 00023000 fc:00 2229409 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/socket.so +7fd2a6516000-7fd2a6517000 r--p 00023000 fc:00 2229409 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/socket.so +7fd2a6517000-7fd2a6518000 rw-p 00024000 fc:00 2229409 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/socket.so +7fd2a6518000-7fd2a651b000 r-xp 00000000 fc:00 2229502 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/digest.so +7fd2a651b000-7fd2a671a000 ---p 00003000 fc:00 2229502 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/digest.so +7fd2a671a000-7fd2a671b000 r--p 00002000 fc:00 2229502 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/digest.so +7fd2a671b000-7fd2a671c000 rw-p 00003000 fc:00 2229502 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/digest.so +7fd2a671c000-7fd2a6732000 r-xp 00000000 fc:00 1573092 /lib/x86_64-linux-gnu/libz.so.1.2.3.4 +7fd2a6732000-7fd2a6931000 ---p 00016000 fc:00 1573092 /lib/x86_64-linux-gnu/libz.so.1.2.3.4 +7fd2a6931000-7fd2a6932000 r--p 00015000 fc:00 1573092 /lib/x86_64-linux-gnu/libz.so.1.2.3.4 +7fd2a6932000-7fd2a6933000 rw-p 00016000 fc:00 1573092 /lib/x86_64-linux-gnu/libz.so.1.2.3.4 +7fd2a6933000-7fd2a6ad2000 r-xp 00000000 fc:00 1572883 /lib/x86_64-linux-gnu/libcrypto.so.1.0.0 +7fd2a6ad2000-7fd2a6cd1000 ---p 0019f000 fc:00 1572883 /lib/x86_64-linux-gnu/libcrypto.so.1.0.0 +7fd2a6cd1000-7fd2a6cec000 r--p 0019e000 fc:00 1572883 /lib/x86_64-linux-gnu/libcrypto.so.1.0.0 +7fd2a6cec000-7fd2a6cf7000 rw-p 001b9000 fc:00 1572883 /lib/x86_64-linux-gnu/libcrypto.so.1.0.0 +7fd2a6cf7000-7fd2a6cfb000 rw-p 00000000 00:00 0 +7fd2a6cfb000-7fd2a6cfc000 r-xp 00000000 fc:00 2229423 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/digest/sha1.so +7fd2a6cfc000-7fd2a6efb000 ---p 00001000 fc:00 2229423 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/digest/sha1.so +7fd2a6efb000-7fd2a6efc000 r--p 00000000 fc:00 2229423 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/digest/sha1.so +7fd2a6efc000-7fd2a6efd000 rw-p 00001000 fc:00 2229423 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/digest/sha1.so +7fd2a6efd000-7fd2a6f00000 r-xp 00000000 fc:00 2229429 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/etc.so +7fd2a6f00000-7fd2a70ff000 ---p 00003000 fc:00 2229429 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/etc.so +7fd2a70ff000-7fd2a7100000 r--p 00002000 fc:00 2229429 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/etc.so +7fd2a7100000-7fd2a7101000 rw-p 00003000 fc:00 2229429 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/etc.so +7fd2a7101000-7fd2a7108000 r-xp 00000000 fc:00 2229501 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/pathname.so +7fd2a7108000-7fd2a7307000 ---p 00007000 fc:00 2229501 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/pathname.so +7fd2a7307000-7fd2a7308000 r--p 00006000 fc:00 2229501 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/pathname.so +7fd2a7308000-7fd2a7309000 rw-p 00007000 fc:00 2229501 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/pathname.so +7fd2a7309000-7fd2a730b000 r-xp 00000000 fc:00 2229473 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/trans/transdb.so +7fd2a730b000-7fd2a750b000 ---p 00002000 fc:00 2229473 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/trans/transdb.so +7fd2a750b000-7fd2a750c000 r--p 00002000 fc:00 2229473 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/trans/transdb.so +7fd2a750c000-7fd2a750d000 rw-p 00003000 fc:00 2229473 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/trans/transdb.so +7fd2a750d000-7fd2a750f000 r-xp 00000000 fc:00 2229441 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/encdb.so +7fd2a750f000-7fd2a770e000 ---p 00002000 fc:00 2229441 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/encdb.so +7fd2a770e000-7fd2a770f000 r--p 00001000 fc:00 2229441 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/encdb.so +7fd2a770f000-7fd2a7710000 rw-p 00002000 fc:00 2229441 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/2.0.0/x86_64-linux/enc/encdb.so +7fd2a7710000-7fd2a79d9000 r--p 00000000 fc:00 923218 /usr/lib/locale/locale-archive +7fd2a79d9000-7fd2a7ad4000 r-xp 00000000 fc:00 1578039 /lib/x86_64-linux-gnu/libm-2.15.so +7fd2a7ad4000-7fd2a7cd3000 ---p 000fb000 fc:00 1578039 /lib/x86_64-linux-gnu/libm-2.15.so +7fd2a7cd3000-7fd2a7cd4000 r--p 000fa000 fc:00 1578039 /lib/x86_64-linux-gnu/libm-2.15.so +7fd2a7cd4000-7fd2a7cd5000 rw-p 000fb000 fc:00 1578039 /lib/x86_64-linux-gnu/libm-2.15.so +7fd2a7cd5000-7fd2a7cde000 r-xp 00000000 fc:00 1578040 /lib/x86_64-linux-gnu/libcrypt-2.15.so +7fd2a7cde000-7fd2a7ede000 ---p 00009000 fc:00 1578040 /lib/x86_64-linux-gnu/libcrypt-2.15.so +7fd2a7ede000-7fd2a7edf000 r--p 00009000 fc:00 1578040 /lib/x86_64-linux-gnu/libcrypt-2.15.so +7fd2a7edf000-7fd2a7ee0000 rw-p 0000a000 fc:00 1578040 /lib/x86_64-linux-gnu/libcrypt-2.15.so +7fd2a7ee0000-7fd2a7f0e000 rw-p 00000000 00:00 0 +7fd2a7f0e000-7fd2a7f10000 r-xp 00000000 fc:00 1578030 /lib/x86_64-linux-gnu/libdl-2.15.so +7fd2a7f10000-7fd2a8110000 ---p 00002000 fc:00 1578030 /lib/x86_64-linux-gnu/libdl-2.15.so +7fd2a8110000-7fd2a8111000 r--p 00002000 fc:00 1578030 /lib/x86_64-linux-gnu/libdl-2.15.so +7fd2a8111000-7fd2a8112000 rw-p 00003000 fc:00 1578030 /lib/x86_64-linux-gnu/libdl-2.15.so +7fd2a8112000-7fd2a8119000 r-xp 00000000 fc:00 1578035 /lib/x86_64-linux-gnu/librt-2.15.so +7fd2a8119000-7fd2a8318000 ---p 00007000 fc:00 1578035 /lib/x86_64-linux-gnu/librt-2.15.so +7fd2a8318000-7fd2a8319000 r--p 00006000 fc:00 1578035 /lib/x86_64-linux-gnu/librt-2.15.so +7fd2a8319000-7fd2a831a000 rw-p 00007000 fc:00 1578035 /lib/x86_64-linux-gnu/librt-2.15.so +7fd2a831a000-7fd2a8332000 r-xp 00000000 fc:00 1578033 /lib/x86_64-linux-gnu/libpthread-2.15.so +7fd2a8332000-7fd2a8531000 ---p 00018000 fc:00 1578033 /lib/x86_64-linux-gnu/libpthread-2.15.so +7fd2a8531000-7fd2a8532000 r--p 00017000 fc:00 1578033 /lib/x86_64-linux-gnu/libpthread-2.15.so +7fd2a8532000-7fd2a8533000 rw-p 00018000 fc:00 1578033 /lib/x86_64-linux-gnu/libpthread-2.15.so +7fd2a8533000-7fd2a8537000 rw-p 00000000 00:00 0 +7fd2a8537000-7fd2a86ec000 r-xp 00000000 fc:00 1578031 /lib/x86_64-linux-gnu/libc-2.15.so +7fd2a86ec000-7fd2a88eb000 ---p 001b5000 fc:00 1578031 /lib/x86_64-linux-gnu/libc-2.15.so +7fd2a88eb000-7fd2a88ef000 r--p 001b4000 fc:00 1578031 /lib/x86_64-linux-gnu/libc-2.15.so +7fd2a88ef000-7fd2a88f1000 rw-p 001b8000 fc:00 1578031 /lib/x86_64-linux-gnu/libc-2.15.so +7fd2a88f1000-7fd2a88f6000 rw-p 00000000 00:00 0 +7fd2a88f6000-7fd2a8b4a000 r-xp 00000000 fc:00 2229274 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/libruby.so.2.0.0 +7fd2a8b4a000-7fd2a8d49000 ---p 00254000 fc:00 2229274 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/libruby.so.2.0.0 +7fd2a8d49000-7fd2a8d4e000 r--p 00253000 fc:00 2229274 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/libruby.so.2.0.0 +7fd2a8d4e000-7fd2a8d51000 rw-p 00258000 fc:00 2229274 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/libruby.so.2.0.0 +7fd2a8d51000-7fd2a8d72000 rw-p 00000000 00:00 0 +7fd2a8d72000-7fd2a8d94000 r-xp 00000000 fc:00 1578045 /lib/x86_64-linux-gnu/ld-2.15.so +7fd2a8e6c000-7fd2a8e6d000 r--s 00000000 fc:00 2231868 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/gems/2.0.0/bin/rake +7fd2a8e7f000-7fd2a8f86000 rw-p 00000000 00:00 0 +7fd2a8f86000-7fd2a8f87000 r--s 00000000 fc:00 2231868 /home/jam/.rvm/rubies/ruby-2.0.0-p247/lib/ruby/gems/2.0.0/bin/rake +7fd2a8f87000-7fd2a8f8e000 r--s 00000000 fc:00 929597 /usr/lib/x86_64-linux-gnu/gconv/gconv-modules.cache +7fd2a8f8e000-7fd2a8f8f000 ---p 00000000 00:00 0 +7fd2a8f8f000-7fd2a8f94000 rw-p 00000000 00:00 0 +7fd2a8f94000-7fd2a8f95000 r--p 00022000 fc:00 1578045 /lib/x86_64-linux-gnu/ld-2.15.so +7fd2a8f95000-7fd2a8f97000 rw-p 00023000 fc:00 1578045 /lib/x86_64-linux-gnu/ld-2.15.so +7fff9c15f000-7fff9c1e3000 rw-p 00000000 00:00 0 [stack] +7fff9c1ff000-7fff9c200000 r-xp 00000000 00:00 0 [vdso] +ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall] + + +[NOTE] +You may have encountered a bug in the Ruby interpreter or extension libraries. +Bug reports are welcome. +For details: http://www.ruby-lang.org/bugreport.html + +Aborted (core dumped) diff --git a/web/spec/controllers/api_affiliate_controller_spec.rb b/web/spec/controllers/api_affiliate_controller_spec.rb new file mode 100644 index 000000000..93306844d --- /dev/null +++ b/web/spec/controllers/api_affiliate_controller_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe ApiAffiliateController do + render_views + + let(:partner1) {FactoryGirl.create(:affiliate_partner)} + let(:user_partner1) { partner1.partner_user } + let(:partner2) {FactoryGirl.create(:affiliate_partner)} + let(:user_partner2) { partner2.partner_user } + + before(:each) do + controller.current_user = user_partner1 + end + + describe "traffic_index" do + it "empty" do + get :traffic_index + response.should be_success + JSON.parse(response.body)['traffics'].should eq([]) + end + + it "single item" do + traffic_total = FactoryGirl.create(:affiliate_traffic_total, affiliate_partner: partner1, visits: 5, signups: 0, day:Date.today) + + get :traffic_index + response.should be_success + JSON.parse(response.body)['traffics'].should eq([{"day" => Date.today.to_s, "visits" => 5, "signups" => 0, "affiliate_partner_id" => partner1.id}]) + end + end + + describe "monthly_index" do + it "empty" do + get :monthly_index + response.should be_success + JSON.parse(response.body)['monthlies'].should eq([]) + end + + it "single item" do + monthly = FactoryGirl.create(:affiliate_monthly_payment, affiliate_partner: partner1, closed: true, due_amount_in_cents: 20, month: 1, year: 2015) + + get :monthly_index + response.should be_success + JSON.parse(response.body)['monthlies'].should eq([{"closed" => true, "month" => 1, "year" => 2015, "due_amount_in_cents" => 20, "affiliate_partner_id" => partner1.id}]) + end + end + + + describe "quarterly_index" do + it "empty" do + get :quarterly_index + response.should be_success + JSON.parse(response.body)['quarterlies'].should eq([]) + end + + it "single item" do + quarterly = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner1, paid: true, closed: true, due_amount_in_cents: 20, quarter: 1, year: 2015) + + get :quarterly_index + response.should be_success + JSON.parse(response.body)['quarterlies'].should eq([{"closed" => true, "paid" => true, "quarter" => 1, "year" => 2015, "due_amount_in_cents" => 20, "affiliate_partner_id" => partner1.id}]) + end + + + it "not paid is excluded" do + quarterly = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner1, paid: false, closed: true, due_amount_in_cents: 20, quarter: 1, year: 2015) + + get :quarterly_index + response.should be_success + JSON.parse(response.body)['quarterlies'].should eq([]) + end + + + it "not closed is excluded" do + quarterly = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner: partner1, paid: true, closed: false, due_amount_in_cents: 20, quarter: 1, year: 2015) + + get :quarterly_index + response.should be_success + JSON.parse(response.body)['quarterlies'].should eq([]) + end + end + +end diff --git a/web/spec/controllers/api_jam_tracks_controller_spec.rb b/web/spec/controllers/api_jam_tracks_controller_spec.rb index fc4685eb9..74bc1da94 100644 --- a/web/spec/controllers/api_jam_tracks_controller_spec.rb +++ b/web/spec/controllers/api_jam_tracks_controller_spec.rb @@ -15,6 +15,8 @@ describe ApiJamTracksController do end before(:each) do + FingerprintWhitelist.destroy_all + FraudAlert.destroy_all MachineFingerprint.destroy_all JamTrackRight.destroy_all JamTrack.destroy_all @@ -299,6 +301,9 @@ describe ApiJamTracksController do describe "guard fraud" do + after(:each) do + ENV['RAILS_TEST_IP_ADDRESS'] = nil + end it "stops second user from downloading using same machine" do right = JamTrackRight.create(:user=>@user, :jam_track=>@jam_track) @@ -306,6 +311,7 @@ describe ApiJamTracksController do right.save! get :download, :id=>@jam_track.id, sample_rate: 48, all_fp: 'all', running_fp: 'running' response.status.should == 202 + MachineFingerprint.count.should eq(2) user2 = FactoryGirl.create(:user) @@ -315,8 +321,10 @@ describe ApiJamTracksController do right.redeemed = true right.save! get :download, :id=>@jam_track.id, sample_rate: 48, all_fp: 'all', running_fp: 'running' - response.status.should == 403 - JSON.parse(response.body)['message'].should eq("other user has 'all' fingerprint") + response.status.should == 202 + + # no error for the user... but we should have a FraudAlert + FraudAlert.count.should eq(1) end it "stops second user from enqueuing using same machine" do @@ -336,9 +344,10 @@ describe ApiJamTracksController do right.save! right.signing_queued_at.should be_nil post :enqueue, {:format=>'json', :id=>right.jam_track.id, fingerprint: {all: 'all', running: 'running'} } + response.status.should == 200 get :download, :id=>@jam_track.id, sample_rate: 48, all_fp: 'all', running_fp: 'running' - response.status.should == 403 - JSON.parse(response.body)['message'].should eq("other user has 'all' fingerprint") + response.status.should == 202 + FraudAlert.count.should eq(1) end end end diff --git a/web/spec/controllers/api_links_controller_spec.rb b/web/spec/controllers/api_links_controller_spec.rb new file mode 100644 index 000000000..297789693 --- /dev/null +++ b/web/spec/controllers/api_links_controller_spec.rb @@ -0,0 +1,106 @@ +require 'spec_helper' + +describe ApiLinksController do + render_views + + let(:partner1) {FactoryGirl.create(:affiliate_partner)} + let(:user_partner1) { partner1.partner_user } + + before(:each) do + ClaimedRecording.delete_all + Recording.delete_all + MusicSession.delete_all + JamTrackRight.delete_all + JamTrack.delete_all + controller.current_user = nil + end + + describe "jamtrack_song_index" do + it "succeeds with empty data" do + get :jamtrack_song_index, format:'json', affiliate_id: partner1.id + response.status.should eq(200) + body = JSON.parse(response.body) + body.length.should eq(0) + end + + it "succeeds with data" do + jam_track = FactoryGirl.create(:jam_track) + get :jamtrack_song_index, format:'json', affiliate_id: partner1.id + response.status.should eq(200) + body = JSON.parse(response.body) + body.length.should eq(1) + body[0]['url'].should_not be_nil + body[0]['target'].should_not be_nil + end + end + + describe "jamkazam_general_index" do + it "succeeds" do + get :jamkazam_general_index, format:'json', affiliate_id: partner1.id + response.status.should eq(200) + body = JSON.parse(response.body) + body.length.should eq(3) + end + end + + + describe "session_index" do + let(:open_params) { + { + name: "session 1", + description: "my session", + genres: ['ambient'], + musician_access: true, + fan_access: true, + approval_required: true, + fan_chat: true, + legal_policy: 'Standard', + language: 'eng', + start: "Thu Jul 10 2020 10:00 PM", + duration: 30, + timezone: "Central Time (US & Canada),America/Chicago", + open_rsvps: true, + legal_terms: true, + recurring_mode: 'once', + isUnstructuredRsvp: true, + rsvp_slots: [{ instrument_id: "other", proficiency_level: 1, approve: true}] + } + } + + it "succeeds with no data" do + get :session_index, format:'json', affiliate_id: partner1.id + response.status.should eq(200) + body = JSON.parse(response.body) + body.length.should eq(0) + end + + + it "succeeds with one scheduled session" do + session = MusicSession.create(user_partner1, open_params) + get :session_index, format:'json', affiliate_id: partner1.id + response.status.should eq(200) + body = JSON.parse(response.body) + body.length.should eq(1) + end + end + + describe "recording_index" do + + it "succeeds with no data" do + get :recording_index, format:'json', affiliate_id: partner1.id + response.status.should eq(200) + body = JSON.parse(response.body) + body.length.should eq(0) + end + + it "succeeds with one recording" do + claimed_recording1 = FactoryGirl.create(:claimed_recording, user: user_partner1) + puts claimed_recording1.inspect + get :recording_index, format:'json', affiliate_id: partner1.id + response.status.should eq(200) + body = JSON.parse(response.body) + body.length.should eq(1) + end + end + +end diff --git a/web/spec/factories.rb b/web/spec/factories.rb index 6a87332bd..5b8153f3f 100644 --- a/web/spec/factories.rb +++ b/web/spec/factories.rb @@ -777,4 +777,37 @@ FactoryGirl.define do transaction_type JamRuby::RecurlyTransactionWebHook::FAILED_PAYMENT end end + + factory :affiliate_partner, class: 'JamRuby::AffiliatePartner' do + sequence(:partner_name) { |n| "partner-#{n}" } + entity_type 'Individual' + signed_at Time.now + association :partner_user, factory: :user + end + + factory :affiliate_quarterly_payment, class: 'JamRuby::AffiliateQuarterlyPayment' do + year 2015 + quarter 0 + association :affiliate_partner, factory: :affiliate_partner + end + + factory :affiliate_monthly_payment, class: 'JamRuby::AffiliateMonthlyPayment' do + year 2015 + month 1 + association :affiliate_partner, factory: :affiliate_partner + end + + factory :affiliate_referral_visit, class: 'JamRuby::AffiliateReferralVisit' do + ip_address '1.1.1.1' + association :affiliate_partner, factory: :affiliate_partner + end + + factory :affiliate_traffic_total, class: 'JamRuby::AffiliateTrafficTotal' do + day Date.today + association :affiliate_partner, factory: :affiliate_partner + end + + factory :affiliate_legalese, class: 'JamRuby::AffiliateLegalese' do + legalese Faker::Lorem.paragraphs(6).join("\n\n") + end end diff --git a/web/spec/features/account_affiliate_spec.rb b/web/spec/features/account_affiliate_spec.rb new file mode 100644 index 000000000..9747a373b --- /dev/null +++ b/web/spec/features/account_affiliate_spec.rb @@ -0,0 +1,152 @@ +require 'spec_helper' + +describe "Account Affiliate", :js => true, :type => :feature, :capybara_feature => true do + + subject { page } + + let(:user) {FactoryGirl.create(:user)} + let(:partner) { FactoryGirl.create(:affiliate_partner) } + let(:jam_track) {FactoryGirl.create(:jam_track)} + + before(:each) do + JamTrackRight.delete_all + JamTrack.delete_all + AffiliateQuarterlyPayment.delete_all + AffiliateMonthlyPayment.delete_all + AffiliateTrafficTotal.delete_all + UserMailer.deliveries.clear + emulate_client + end + + describe "account overview" do + it "shows correct values for partner" do + partner.referral_user_count = 3 + partner.cumulative_earnings_in_cents = 10000 + partner.save! + + sign_in_poltergeist partner.partner_user + visit "/client#/account" + find('.account-mid.affiliate .user-referrals', text: 'You have referred 3 users to date.') + find('.account-mid.affiliate .affiliate-earnings', text: 'You have earned $100.00 to date.') + end + + it "shows correct values for unaffiliated user" do + sign_in_poltergeist user + visit "/client#/account" + find('.account-mid.affiliate .not-affiliated', text: 'You are not currently a JamKazam affiliate.') + find('.account-mid.affiliate .learn-affiliate') + end + end + + describe "account affiliate page" do + before(:each) do + sign_in_poltergeist partner.partner_user + end + + it "works on no data" do + jam_track.touch + + visit "/client#/account/affiliatePartner" + find('.tab-account', text: 'So please provide this data, and be sure to keep it current!') + + # take a look at the links tab + find('a#affiliate-partner-links-link').trigger(:click) + + find('#account-affiliate-partner tr td.target') + + # can't find this on the page for some reason: + #jk_select('Custom Link', '#account-affiliate-partner select.link_type') + #find('.link-type-prompt[data-type="custom_links"]') + + find('a#affiliate-partner-signups-link').trigger(:click) + find('table.traffic-table') + + find('a#affiliate-partner-earnings-link').trigger(:click) + find('table.payment-table') + + find('a#affiliate-partner-agreement-link').trigger(:click) + find('h2', text: 'JamKazam Affiliate Agreement') + find('span.c0', text: 'Updated: April 30, 2015') + end + + it "shows data" do + visit "/client#/account/affiliatePartner" + find('.tab-account', text: 'So please provide this data, and be sure to keep it current!') + + # verify traffic data shows correctly + day1 = Date.parse('2015-04-05') + FactoryGirl.create(:affiliate_traffic_total, affiliate_partner: partner, day: day1, signups: 1, visits:2) + + find('a#affiliate-partner-signups-link').trigger(:click) + find('table.traffic-table tr td.day', text: "April 5") + find('table.traffic-table tr td.signups', text: '1') + find('table.traffic-table tr td.visits', text: '2') + + + find('a#affiliate-partner-earnings-link').trigger(:click) + find('table.payment-table') + + day2 = Date.parse('2015-04-07') + FactoryGirl.create(:affiliate_traffic_total, affiliate_partner: partner, day: day2, signups: 3, visits:4) + find('a#affiliate-partner-signups-link').trigger(:click) + find('table.traffic-table tr td.day', text: "April 7") + find('table.traffic-table tr td.signups', text: '3') + find('table.traffic-table tr td.visits', text: '4') + + # verify earnings data correctly + FactoryGirl.create(:affiliate_monthly_payment, affiliate_partner:partner, year:2015, month:1, due_amount_in_cents:20, jamtracks_sold: 1, closed:true) + + find('a#affiliate-partner-earnings-link').trigger(:click) + find('table.payment-table tr td.month', text: "January 2015") + find('table.payment-table tr td.sales', text: 'JamTracks: 1 unit sold') + find('table.payment-table tr td.earnings', text: '$0.20') + + + find('a#affiliate-partner-signups-link').trigger(:click) + find('table.traffic-table') + + FactoryGirl.create(:affiliate_monthly_payment, affiliate_partner:partner, year:2015, month:2, due_amount_in_cents:40, jamtracks_sold: 2, closed:true) + FactoryGirl.create(:affiliate_monthly_payment, affiliate_partner:partner, year:2015, month:3, due_amount_in_cents:60, jamtracks_sold: 3, closed:true) + quarter1 = FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner:partner, year:2015, quarter:0, due_amount_in_cents:120, jamtracks_sold: 6, closed:true, paid:false) + FactoryGirl.create(:affiliate_monthly_payment, affiliate_partner:partner, year:2015, month:4, due_amount_in_cents:2000, jamtracks_sold: 100, closed:true) + + find('a#affiliate-partner-earnings-link').trigger(:click) + find('table.payment-table tr td.month', text: "January 2015") + find('table.payment-table tr td.sales', text: 'JamTracks: 1 unit sold') + find('table.payment-table tr td.earnings', text: '$0.20') + find('table.payment-table tr td.month', text: "February 2015") + find('table.payment-table tr td.sales', text: 'JamTracks: 2 units sold') + find('table.payment-table tr td.earnings', text: '$0.40') + find('table.payment-table tr td.month', text: "March 2015") + find('table.payment-table tr td.sales', text: 'JamTracks: 3 units sold') + find('table.payment-table tr td.earnings', text: '$0.60') + find('table.payment-table tr td.month', text: "1st Quarter 2015") + find('table.payment-table tr td.earnings', text: 'No earning were paid, as the $10 minimum threshold was not reached.') + + + + find('a#affiliate-partner-signups-link').trigger(:click) + find('table.traffic-table') + + quarter1.paid = true + quarter1.save! + FactoryGirl.create(:affiliate_quarterly_payment, affiliate_partner:partner, year:2015, quarter:1, due_amount_in_cents:2000, jamtracks_sold: 100, closed:true, paid:true) + + find('a#affiliate-partner-earnings-link').trigger(:click) + find('table.payment-table tr td.month', text: "January 2015") + find('table.payment-table tr td.sales', text: 'JamTracks: 1 unit sold') + find('table.payment-table tr td.earnings', text: '$0.20') + find('table.payment-table tr td.month', text: "February 2015") + find('table.payment-table tr td.sales', text: 'JamTracks: 2 units sold') + find('table.payment-table tr td.earnings', text: '$0.40') + find('table.payment-table tr td.month', text: "March 2015") + find('table.payment-table tr td.sales', text: 'JamTracks: 3 units sold') + find('table.payment-table tr td.earnings', text: '$0.60') + + find('table.payment-table tr td.month', text: "1st Quarter 2015") + find('table.payment-table tr td.earnings', text: 'PAID $1.20') + find('table.payment-table tr td.month', text: "2nd Quarter 2015") + find('table.payment-table tr td.earnings', text: 'PAID $20.00') + end + end +end diff --git a/web/spec/features/affiliate_program_spec.rb b/web/spec/features/affiliate_program_spec.rb new file mode 100644 index 000000000..26378cd92 --- /dev/null +++ b/web/spec/features/affiliate_program_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe "Affiliate Program", :js => true, :type => :feature, :capybara_feature => true do + + subject { page } + + let(:user) { FactoryGirl.create(:user) } + + before(:each) do + User.delete_all + AffiliateQuarterlyPayment.delete_all + AffiliateMonthlyPayment.delete_all + AffiliateTrafficTotal.delete_all + AffiliatePartner.delete_all + end + + before(:all) do + @old_recaptcha=Rails.application.config.recaptcha_enable + Rails.application.config.recaptcha_enable=false + end + + after(:all) do + Rails.application.config.recaptcha_enable=@old_recaptcha + end + + describe "Affiliate Program signup page" do + it "logged in user creates affiliate" do + fast_signin user, '/affiliateProgram' + + find('input#entity_individual').trigger(:click) + + find('.agree-button').trigger(:click) + + find('h1', text: 'congratulations') + find('.button-orange', text: 'GO TO AFFILIATE PAGE').trigger(:click) + + find('.tab-account', text: 'So please provide this data, and be sure to keep it current!') + + partner = AffiliatePartner.first + partner.partner_user.should eq(user) + partner.entity_type.should eq('Individual') + end + + it "logged in user creates entity affiliate" do + fast_signin user, '/affiliateProgram' + + find('input#entity_entity').trigger(:click) + fill_in('entity-name', with: 'Mr. Bubbles') + select('Sole Proprietor', from:'entity-type') + + find('.agree-button').trigger(:click) + + find('h1', text: 'congratulations') + find('.button-orange', text: 'GO TO AFFILIATE PAGE').trigger(:click) + + find('.tab-account', text: 'So please provide this data, and be sure to keep it current!') + + partner = AffiliatePartner.first + partner.partner_user.should eq(user) + partner.entity_type.should eq('Sole Proprietor') + end + + it "new user creates individual affiliate" do + visit '/affiliateProgram' + + find('input#entity_individual').trigger(:click) + + find('.agree-button').trigger(:click) + + find('h1', text: 'congratulations') + find('.button-orange', text: 'GO SIGNUP').trigger(:click) + + fill_in "jam_ruby_user[first_name]", with: "Affiliate1" + fill_in "jam_ruby_user[last_name]", with: "Someone" + fill_in "jam_ruby_user[email]", with: "affiliate1@jamkazam.com" + fill_in "jam_ruby_user[password]", with: "jam123" + fill_in "jam_ruby_user[password_confirmation]", with: "jam123" + check("jam_ruby_user[instruments][drums][selected]") + check("jam_ruby_user[terms_of_service]") + click_button "CREATE ACCOUNT" + + should have_title("JamKazam | Congratulations") + + found_user = User.first + partner = AffiliatePartner.first + partner.partner_user.should eq(found_user) + partner.entity_type.should eq('Individual') + end + end +end diff --git a/web/spec/features/affiliate_referral_spec.rb b/web/spec/features/affiliate_referral_spec.rb new file mode 100644 index 000000000..ed4f24f49 --- /dev/null +++ b/web/spec/features/affiliate_referral_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe "affiliate visit tracking", :js => true, :type => :feature, :capybara_feature => true do + + subject { page } + + let(:user) { FactoryGirl.create(:user) } + let(:partner) { FactoryGirl.create(:affiliate_partner) } + let(:affiliate_params) { partner.affiliate_query_params } + + before(:each) do + AffiliateTrafficTotal.delete_all + AffiliateReferralVisit.delete_all + end + + before(:all) do + @old_recaptcha=Rails.application.config.recaptcha_enable + Rails.application.config.recaptcha_enable=false + end + + after(:all) do + Rails.application.config.recaptcha_enable=@old_recaptcha + end + + # the ways that a user might signup: + # on free jamtrack redemption flow - this is verified in checkout_spec.rb + # via facebook + # /signup + it "verifies that a signup via /signup page, when coming from a referral, attributes partner" do + visit '/?' + affiliate_params + + should_be_at_root + AffiliateReferralVisit.count.should eq(1) + + visit '/signup' + fill_in "jam_ruby_user[first_name]", with: "Affiliate" + fill_in "jam_ruby_user[last_name]", with: "Referral" + fill_in "jam_ruby_user[email]", with: "referral1@jamkazam.com" + fill_in "jam_ruby_user[password]", with: "jam123" + fill_in "jam_ruby_user[password_confirmation]", with: "jam123" + check("jam_ruby_user[instruments][drums][selected]") + check("jam_ruby_user[terms_of_service]") + click_button "CREATE ACCOUNT" + + should have_title("JamKazam | Congratulations") + + referral = User.find_by_email('referral1@jamkazam.com') + referral.affiliate_referral.should eq(partner) + AffiliatePartner.tally_up(referral.created_at.to_date + 1) + + partner.reload + partner.referral_user_count.should eq(1) + + total = AffiliateTrafficTotal.first + total.signups.should eq(1) + end + + +end diff --git a/web/spec/features/affiliate_visit_tracking.rb b/web/spec/features/affiliate_visit_tracking.rb new file mode 100644 index 000000000..09ee9dcd5 --- /dev/null +++ b/web/spec/features/affiliate_visit_tracking.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe "affiliate visit tracking" do + + subject { page } + + let(:user) { FactoryGirl.create(:user) } + let(:partner) { FactoryGirl.create(:affiliate_partner) } + let(:affiliate_params) { partner.affiliate_query_params } + + before(:each) do + AffiliateReferralVisit.delete_all + end + + it "tracks" do + visit '/?' + affiliate_params + + should_be_at_root + AffiliateReferralVisit.count.should eq(1) + visit = AffiliateReferralVisit.first + visit.visited_url.should eq('/?' + affiliate_params) + visit.affiliate_partner_id.should eq(partner.id) + visit.first_visit.should be_true + + download_url = '/downloads?' + affiliate_params + visit download_url + find('h2.create-account-header') + + + AffiliateReferralVisit.count.should eq(2) + visit = AffiliateReferralVisit.find_by_visited_url(download_url) + visit.affiliate_partner_id.should eq(partner.id) + visit.first_visit.should be_false + end + +end diff --git a/web/spec/features/checkout_spec.rb b/web/spec/features/checkout_spec.rb index d0a3a9b0c..bebe03a02 100644 --- a/web/spec/features/checkout_spec.rb +++ b/web/spec/features/checkout_spec.rb @@ -827,6 +827,123 @@ describe "Checkout", :js => true, :type => :feature, :capybara_feature => true d end + it "for anonymous user with referral" do + + partner = FactoryGirl.create(:affiliate_partner) + affiliate_params = partner.affiliate_query_params + + visit '/landing/jamtracks/acdc-backinblack?' + affiliate_params + find('a.browse-jamtracks').trigger(:click) + + find('h1', text: 'jamtracks') + #find('a', text: 'What is a JamTrack?') + + find("a.jamtrack-add-cart[data-jamtrack-id=\"#{jamtrack_acdc_backinblack.id}\"]", text: 'GET IT FREE!').trigger(:click) + find('h3', text: 'OR SIGN UP USING YOUR EMAIL') + shopping_carts = ShoppingCart.all + shopping_carts.count.should eq(1) + shopping_cart = shopping_carts[0] + shopping_cart.anonymous_user_id.should_not be_nil + shopping_cart.user_id.should be_nil + + fill_in 'first_name', with: 'Seth' + fill_in 'last_name', with: 'Call' + fill_in 'email', with: 'guy_referral@jamkazam.com' + fill_in 'password', with: 'jam123' + find('.right-side .terms_of_service input').trigger(:click) # accept TOS + + # try to submit, and see order page + find('.signup-submit').trigger(:click) + + find('.jam-tracks-in-browser') + + guy = User.find_by_email('guy_referral@jamkazam.com') + guy.affiliate_referral.should eq(partner) + guy.reload + + # verify sales data + guy.sales.length.should eq(1) + sale = guy.sales.first + sale.sale_line_items.length.should eq(1) + acdc_sale = sale.sale_line_items[0] + acdc_sale.affiliate_referral.should eq(partner) + acdc_sale.affiliate_refunded.should be_false + acdc_sale.affiliate_refunded_at.should be_nil + acdc_sale.affiliate_referral_fee_in_cents.should eq(0) + + # this is a little cheat to make the billing info dropdown already say US + guy.country = 'US' + guy.save! + + # now, go back to checkout flow again, and make sure we are told there are no free jam tracks + + visit "/client#/jamtrackBrowse" + + find("a.jamtrack-add-cart[data-jamtrack-id=\"#{jamtrack_pearljam_evenflow.id}\"]", text: 'ADD TO CART').trigger(:click) + find('h1', text: 'shopping cart') + find('.cart-item-caption', text: "JamTrack: #{jamtrack_pearljam_evenflow.name}") + find('.cart-item-price', text: "$ #{jamtrack_pearljam_evenflow.price}") + find('.shopping-sub-total', text:"Subtotal:$ #{jamtrack_pearljam_evenflow.price}") + + # attempt to checkout + find('a.button-orange', text: 'PROCEED TO CHECKOUT').trigger(:click) + + # should be at payment page + + # this should take us to the payment screen + find('p.payment-prompt') + + # fill out all billing info and account info + fill_in 'billing-first-name', with: 'Seth' + fill_in 'billing-last-name', with: 'Call' + fill_in 'billing-address1', with: '10704 Buckthorn Drive' + fill_in 'billing-city', with: 'Austin' + fill_in 'billing-state', with: 'Texas' + fill_in 'billing-zip', with: '78759' + fill_in 'card-number', with: '4111111111111111' + fill_in 'card-verify', with: '012' + #jk_select('US', '#checkoutPaymentScreen #divBillingCountry #billing-country') + + find('#payment-info-next').trigger(:click) + + # should be taken straight to order page + + # now see order page, and everything should no longer appear free + find('p.order-prompt') + find('.order-items-value.order-total', text:'$1.99') + find('.order-items-value.shipping-handling', text:'$0.00') + find('.order-items-value.sub-total', text:'$1.99') + find('.order-items-value.taxes', text:'$0.16') + find('.order-items-value.grand-total', text:'$2.15') + + # click the ORDER button + find('.place-order-center a.button-orange.place-order').trigger(:click) + + # and now we should see confirmation, and a notice that we are in a normal browser + find('.thanks-detail.jam-tracks-in-browser') + + guy.reload + + sleep 3 # challenge to all comers! WHY DO I HAVE TO SLEEP FOR THIS ASSERTION TO BE TRUE! GAH . and 1 second won't do it + + jam_track_right = jamtrack_pearljam_evenflow.right_for_user(guy) + # make sure it appears the user actually bought the jamtrack! + jam_track_right.should_not be_nil + jam_track_right.redeemed.should be_false + guy.has_redeemable_jamtrack.should be_false + + # verify sales data + guy.sales.length.should eq(2) + sale = guy.sales.last + sale.sale_line_items.length.should eq(1) + acdc_sale = SaleLineItem.find_by_recurly_adjustment_uuid(jam_track_right.recurly_adjustment_uuid) + acdc_sale.affiliate_referral.should eq(partner) + acdc_sale.affiliate_refunded.should be_false + acdc_sale.affiliate_refunded_at.should be_nil + acdc_sale.affiliate_referral_fee_in_cents.should eq(20) + + end + it "for existing user with a freebie available (already logged in)" do diff --git a/web/spec/managers/user_manager_spec.rb b/web/spec/managers/user_manager_spec.rb index 275d90b69..f6d464226 100644 --- a/web/spec/managers/user_manager_spec.rb +++ b/web/spec/managers/user_manager_spec.rb @@ -182,9 +182,7 @@ describe UserManager do describe "signup" do let!(:user) { FactoryGirl.create(:user) } let!(:partner) { - AffiliatePartner.create_with_params({:partner_name => Faker::Company.name, - :partner_code => Faker::Lorem.words(1)[0], - :user_email => user.email}) + AffiliatePartner.create_with_web_params(user, {:partner_name => Faker::Company.name, entity_type:'Individual'}) } it "signup successfully" do @@ -200,7 +198,7 @@ describe UserManager do instruments: @instruments, musician:true, signup_confirm_url: "http://localhost:3000/confirm", - affiliate_referral_id: AffiliatePartner.coded_id(partner.partner_code)) + affiliate_referral_id: partner.id) user.errors.any?.should be_false user.first_name.should == "bob" diff --git a/web/spec/requests/affilate_referral_spec.rb b/web/spec/requests/affilate_referral_spec.rb index 7a61f8ec4..c3d0f457d 100644 --- a/web/spec/requests/affilate_referral_spec.rb +++ b/web/spec/requests/affilate_referral_spec.rb @@ -6,9 +6,7 @@ describe "Affiliate Reports", :type => :api do let!(:user) { FactoryGirl.create(:user) } let!(:partner) { - AffiliatePartner.create_with_params({:partner_name => Faker::Company.name, - :partner_code => Faker::Lorem.words[0], - :user_email => user.email}) + AffiliatePartner.create_with_web_params(user, {:partner_name => Faker::Company.name, entity_type:'Individual'}) } it "valid score" do diff --git a/web/spec/support/app_config.rb b/web/spec/support/app_config.rb index b7cc08d89..6ee586981 100644 --- a/web/spec/support/app_config.rb +++ b/web/spec/support/app_config.rb @@ -94,6 +94,14 @@ def web_config def secret_token 'ced345e01611593c1b783bae98e4e56dbaee787747e92a141565f7c61d0ab2c6f98f7396fb4b178258301e2713816e158461af58c14b695901692f91e72b6200' end + + def expire_fingerprint_days + 14 + end + + def found_conflict_count + 1 + end end klass.new end diff --git a/web/spec/support/remote_ip_monkey_patch.rb b/web/spec/support/remote_ip_monkey_patch.rb new file mode 100644 index 000000000..7d725d072 --- /dev/null +++ b/web/spec/support/remote_ip_monkey_patch.rb @@ -0,0 +1,17 @@ +module ActionDispatch + class Request + + def remote_ip_with_mocking + test_ip = ENV['RAILS_TEST_IP_ADDRESS'] + + unless test_ip.nil? or test_ip.empty? + return test_ip + else + return remote_ip_without_mocking + end + end + + alias_method_chain :remote_ip, :mocking + + end +end \ No newline at end of file diff --git a/web/spec/support/utilities.rb b/web/spec/support/utilities.rb index 85274ad79..83a285a2e 100644 --- a/web/spec/support/utilities.rb +++ b/web/spec/support/utilities.rb @@ -225,7 +225,7 @@ def should_be_at_root(options={signed_in:nil}) find('h1', text: 'Live music platform & social network for musicians') end when Capybara::RackTest::Driver - signed_in = false # actually, the user may be signed in, but, we only redirect to /client in javascript, so RackTest won't do that + signed_in = !cookie_jar['remember_token'].nil? if signed_in find('h2', text: 'create session') else diff --git a/web/vendor/assets/javascripts/bugsnag.js b/web/vendor/assets/javascripts/bugsnag.js new file mode 100644 index 000000000..0bdefaadb --- /dev/null +++ b/web/vendor/assets/javascripts/bugsnag.js @@ -0,0 +1,10 @@ +// 2.4.8 http://d2wy8f7a9ursnm.cloudfront.net/bugsnag-2.4.8.min.js + + +// START COPY/PASTE FROM BUGSNAG CDN +!function(a,b){function c(a,b){try{if("function"!=typeof a)return a;if(!a.bugsnag){var c=e();a.bugsnag=function(d){if(b&&b.eventHandler&&(u=d),v=c,!y){var e=a.apply(this,arguments);return v=null,e}try{return a.apply(this,arguments)}catch(f){throw l("autoNotify",!0)&&(x.notifyException(f,null,null,"error"),s()),f}finally{v=null}},a.bugsnag.bugsnag=a.bugsnag}return a.bugsnag}catch(d){return a}}function d(){B=!1}function e(){var a=document.currentScript||v;if(!a&&B){var b=document.scripts||document.getElementsByTagName("script");a=b[b.length-1]}return a}function f(a){var b=e();b&&(a.script={src:b.src,content:l("inlineScript",!0)?b.innerHTML:""})}function g(b){var c=l("disableLog"),d=a.console;void 0===d||void 0===d.log||c||d.log("[Bugsnag] "+b)}function h(b,c,d){if(d>=5)return encodeURIComponent(c)+"=[RECURSIVE]";d=d+1||1;try{if(a.Node&&b instanceof a.Node)return encodeURIComponent(c)+"="+encodeURIComponent(r(b));var e=[];for(var f in b)if(b.hasOwnProperty(f)&&null!=f&&null!=b[f]){var g=c?c+"["+f+"]":f,i=b[f];e.push("object"==typeof i?h(i,g,d):encodeURIComponent(g)+"="+encodeURIComponent(i))}return e.join("&")}catch(j){return encodeURIComponent(c)+"="+encodeURIComponent(""+j)}}function i(a,b){if(null==b)return a;a=a||{};for(var c in b)if(b.hasOwnProperty(c))try{a[c]=b[c].constructor===Object?i(a[c],b[c]):b[c]}catch(d){a[c]=b[c]}return a}function j(a,b){a+="?"+h(b)+"&ct=img&cb="+(new Date).getTime();var c=new Image;c.src=a}function k(a){var b={},c=/^data\-([\w\-]+)$/;if(a)for(var d=a.attributes,e=0;e\n";var f=[];try{for(var h=arguments.callee.caller.caller;h&&f.length"}return a.nodeName}}function s(){z+=1,a.setTimeout(function(){z-=1})}function t(a,b,c){var d=a[b],e=c(d);a[b]=e}var u,v,w,x={},y=!0,z=0,A=10;x.noConflict=function(){return a.Bugsnag=b,x},x.refresh=function(){A=10},x.notifyException=function(a,b,c,d){b&&"string"!=typeof b&&(c=b,b=void 0),c||(c={}),f(c),n({name:b||a.name,message:a.message||a.description,stacktrace:p(a)||o(),file:a.fileName||a.sourceURL,lineNumber:a.lineNumber||a.line,columnNumber:a.columnNumber?a.columnNumber+1:void 0,severity:d||"warning"},c)},x.notify=function(b,c,d,e){n({name:b,message:c,stacktrace:o(),file:a.location.toString(),lineNumber:1,severity:e||"warning"},d)};var B="complete"!==document.readyState;document.addEventListener?(document.addEventListener("DOMContentLoaded",d,!0),a.addEventListener("load",d,!0)):a.attachEvent("onload",d);var C,D=/^[0-9a-f]{32}$/i,E=/function\s*([\w\-$]+)?\s*\(/i,F="https://notify.bugsnag.com/",G=F+"js",H="2.4.8",I=document.getElementsByTagName("script"),J=I[I.length-1];if(a.atob){if(a.ErrorEvent)try{0===new a.ErrorEvent("test").colno&&(y=!1)}catch(K){}}else y=!1;if(l("autoNotify",!0)){t(a,"onerror",function(b){return function(c,d,e,g,h){var i=l("autoNotify",!0),j={};!g&&a.event&&(g=a.event.errorCharacter),f(j),v=null,i&&!z&&n({name:h&&h.name||"window.onerror",message:c,file:d,lineNumber:e,columnNumber:g,stacktrace:h&&p(h)||o(),severity:"error"},j),b&&b(c,d,e,g,h)}});var L=function(a){return function(b,d){if("function"==typeof b){b=c(b);var e=Array.prototype.slice.call(arguments,2);return a(function(){b.apply(this,e)},d)}return a(b,d)}};t(a,"setTimeout",L),t(a,"setInterval",L),a.requestAnimationFrame&&t(a,"requestAnimationFrame",function(a){return function(b){return a(c(b))}}),a.setImmediate&&t(a,"setImmediate",function(a){return function(){var b=Array.prototype.slice.call(arguments);return b[0]=c(b[0]),a.apply(this,b)}}),"EventTarget Window Node ApplicationCache AudioTrackList ChannelMergerNode CryptoOperation EventSource FileReader HTMLUnknownElement IDBDatabase IDBRequest IDBTransaction KeyOperation MediaController MessagePort ModalWindow Notification SVGElementInstance Screen TextTrack TextTrackCue TextTrackList WebSocket WebSocketWorker Worker XMLHttpRequest XMLHttpRequestEventTarget XMLHttpRequestUpload".replace(/\w+/g,function(b){var d=a[b]&&a[b].prototype;d&&d.hasOwnProperty&&d.hasOwnProperty("addEventListener")&&(t(d,"addEventListener",function(a){return function(b,d,e,f){try{d&&d.handleEvent&&(d.handleEvent=c(d.handleEvent,{eventHandler:!0}))}catch(h){g(h)}return a.call(this,b,c(d,{eventHandler:!0}),e,f)}}),t(d,"removeEventListener",function(a){return function(b,d,e,f){return a.call(this,b,d,e,f),a.call(this,b,c(d),e,f)}}))})}a.Bugsnag=x,"function"==typeof define&&define.amd?define([],function(){return x}):"object"==typeof module&&"object"==typeof module.exports&&(module.exports=x)}(window,window.Bugsnag); +// END COPY/PASTE FROM BUGSNAG CDN + + +// manual code: make sure Bugsnag has it's API KEY +window.Bugsnag.apiKey = gon.global.bugsnag_key \ No newline at end of file diff --git a/web/vendor/assets/javascripts/jquery.visible.js b/web/vendor/assets/javascripts/jquery.visible.js new file mode 100644 index 000000000..1e340099b --- /dev/null +++ b/web/vendor/assets/javascripts/jquery.visible.js @@ -0,0 +1,68 @@ +(function($){ + + /** + * Copyright 2012, Digital Fusion + * Licensed under the MIT license. + * http://teamdf.com/jquery-plugins/license/ + * + * @author Sam Sehnert + * @desc A small plugin that checks whether elements are within + * the user visible viewport of a web browser. + * only accounts for vertical position, not horizontal. + */ + var $w = $(window); + $.fn.visible = function(partial,hidden,direction){ + + if (this.length < 1) + return; + + var $t = this.length > 1 ? this.eq(0) : this, + t = $t.get(0), + vpWidth = $w.width(), + vpHeight = $w.height(), + direction = (direction) ? direction : 'both', + clientSize = hidden === true ? t.offsetWidth * t.offsetHeight : true; + + if (typeof t.getBoundingClientRect === 'function'){ + + // Use this native browser method, if available. + var rec = t.getBoundingClientRect(), + tViz = rec.top >= 0 && rec.top < vpHeight, + bViz = rec.bottom > 0 && rec.bottom <= vpHeight, + lViz = rec.left >= 0 && rec.left < vpWidth, + rViz = rec.right > 0 && rec.right <= vpWidth, + vVisible = partial ? tViz || bViz : tViz && bViz, + hVisible = partial ? lViz || rViz : lViz && rViz; + + if(direction === 'both') + return clientSize && vVisible && hVisible; + else if(direction === 'vertical') + return clientSize && vVisible; + else if(direction === 'horizontal') + return clientSize && hVisible; + } else { + + var viewTop = $w.scrollTop(), + viewBottom = viewTop + vpHeight, + viewLeft = $w.scrollLeft(), + viewRight = viewLeft + vpWidth, + offset = $t.offset(), + _top = offset.top, + _bottom = _top + $t.height(), + _left = offset.left, + _right = _left + $t.width(), + compareTop = partial === true ? _bottom : _top, + compareBottom = partial === true ? _top : _bottom, + compareLeft = partial === true ? _right : _left, + compareRight = partial === true ? _left : _right; + + if(direction === 'both') + return !!clientSize && ((compareBottom <= viewBottom) && (compareTop >= viewTop)) && ((compareRight <= viewRight) && (compareLeft >= viewLeft)); + else if(direction === 'vertical') + return !!clientSize && ((compareBottom <= viewBottom) && (compareTop >= viewTop)); + else if(direction === 'horizontal') + return !!clientSize && ((compareRight <= viewRight) && (compareLeft >= viewLeft)); + } + }; + +})(jQuery); \ No newline at end of file From bf14fa9926918acc5f13626b543c29e1cfd39f0d Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Wed, 3 Jun 2015 16:34:19 -0500 Subject: [PATCH 173/195] VRFS-3246 : Allow parent to be specified to instance of SiteValidator. This allows multiple instances of the same site_type. Also, change the way the validators are invoked. Uses classes instead of ids, again as there are now multiple of a site_type. --- .../javascripts/accounts_profile_samples.js | 65 +++++++++---------- .../javascripts/site_validator.js.coffee | 13 ++-- .../_profile_online_sample_controls.html.slim | 46 ++++++------- 3 files changed, 62 insertions(+), 62 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index aebe9b51e..30cd85c20 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -7,7 +7,7 @@ // TODO: Add a target type, which can be band or user -- call the // appropriate API methods. context.JK.AccountProfileSamples = function(app, parent, loadFn, updateFn) { - var $document = $(document); + var $document = $(document) // used to initialize RecordingSourceValidator in site_validator.js.coffee window.jamkazamRecordingSources = []; @@ -22,14 +22,14 @@ var profileUtils = context.JK.ProfileUtils; var $screen = $('.profile-online-sample-controls', parent); // online presences - var $website = $screen.find('#website'); - var $soundCloudUsername = $screen.find('#soundcloud-username'); - var $reverbNationUsername = $screen.find('#reverbnation-username'); - var $bandCampUsername = $screen.find('#bandcamp-username'); - var $fandalismUsername = $screen.find('#fandalism-username'); - var $youTubeUsername = $screen.find('#youtube-username'); - var $facebookUsername = $screen.find('#facebook-username'); - var $twitterUsername = $screen.find('#twitter-username'); + var $website = $screen.find('.website'); + var $soundCloudUsername = $screen.find('.soundcloud-username'); + var $reverbNationUsername = $screen.find('.reverbnation-username'); + var $bandCampUsername = $screen.find('.bandcamp-username'); + var $fandalismUsername = $screen.find('.fandalism-username'); + var $youTubeUsername = $screen.find('.youtube-username'); + var $facebookUsername = $screen.find('.facebook-username'); + var $twitterUsername = $screen.find('.twitter-username'); // performance samples var $jamkazamSampleList = $screen.find('.samples.jamkazam').find('.sample-list'); @@ -37,16 +37,15 @@ var $youTubeSampleList = $screen.find('.samples.youtube').find('.sample-list'); // buttons - var $btnAddJkRecording = $screen.find('#btn-add-jk-recording'); - var $btnCancel = $screen.find('#account-edit-profile-cancel'); - var $btnBack = $screen.find('#account-edit-profile-back'); - var $btnSubmit = $screen.find('#account-edit-profile-submit'); + var $btnAddJkRecording = $screen.find('.btn-add-jk-recording'); + var $btnCancel = $screen.find('.account-edit-profile-cancel'); + var $btnBack = $screen.find('.account-edit-profile-back'); + var $btnSubmit = $screen.find('.account-edit-profile-submit'); function beforeShow(data) { } - function afterShow(data) { - //$.when(api.getUserProfile()) + function afterShow(data) { $.when(loadFn()) .done(function(targetPlayer) { if (targetPlayer && targetPlayer.keys && targetPlayer.keys.length > 0) { @@ -350,9 +349,9 @@ // This function is a bit of a mess. It was pulled // from the html.erb file verbatim, and could use a // refactor: - function initializeValidators() { + function initializeValidators() { var initialized = false; - $(document).on('JAMKAZAM_READY', function(e, data) { + //$document.on('JAMKAZAM_READY', function(e, data) { window.JK.JamServer.get$Server().on(window.JK.EVENTS.CONNECTION_UP, function() { if(initialized) { return; @@ -360,39 +359,39 @@ initialized = true; //var $screen = $('#account-profile-samples'); - var $btnAddSoundCloudRecording = $screen.find('#btn-add-soundcloud-recording'); - var $btnAddYouTubeVideo = $screen.find('#btn-add-youtube-video'); + var $btnAddSoundCloudRecording = $screen.find('.btn-add-soundcloud-recording'); + var $btnAddYouTubeVideo = $screen.find('.btn-add-youtube-video'); var $soundCloudSampleList = $screen.find('.samples.soundcloud'); var $youTubeSampleList = $screen.find('.samples.youtube'); - setTimeout(function() { - window.urlValidator = new JK.SiteValidator('url', userNameSuccessCallback, userNameFailCallback); + setTimeout(function() { + window.urlValidator = new JK.SiteValidator('url', userNameSuccessCallback, userNameFailCallback, parent); window.urlValidator.init(); - window.soundCloudValidator = new JK.SiteValidator('soundcloud', userNameSuccessCallback, userNameFailCallback); + window.soundCloudValidator = new JK.SiteValidator('soundcloud', userNameSuccessCallback, userNameFailCallback, parent); window.soundCloudValidator.init(); - window.reverbNationValidator = new JK.SiteValidator('reverbnation', userNameSuccessCallback, userNameFailCallback); + window.reverbNationValidator = new JK.SiteValidator('reverbnation', userNameSuccessCallback, userNameFailCallback, parent); window.reverbNationValidator.init(); - window.bandCampValidator = new JK.SiteValidator('bandcamp', userNameSuccessCallback, userNameFailCallback); + window.bandCampValidator = new JK.SiteValidator('bandcamp', userNameSuccessCallback, userNameFailCallback, parent); window.bandCampValidator.init(); - window.fandalismValidator = new JK.SiteValidator('fandalism', userNameSuccessCallback, userNameFailCallback); + window.fandalismValidator = new JK.SiteValidator('fandalism', userNameSuccessCallback, userNameFailCallback, parent); window.fandalismValidator.init(); - window.youTubeValidator = new JK.SiteValidator('youtube', userNameSuccessCallback, userNameFailCallback); + window.youTubeValidator = new JK.SiteValidator('youtube', userNameSuccessCallback, userNameFailCallback, parent); window.youTubeValidator.init(); - window.facebookValidator = new JK.SiteValidator('facebook', userNameSuccessCallback, userNameFailCallback); + window.facebookValidator = new JK.SiteValidator('facebook', userNameSuccessCallback, userNameFailCallback, parent); window.facebookValidator.init(); - window.twitterValidator = new JK.SiteValidator('twitter', userNameSuccessCallback, userNameFailCallback); + window.twitterValidator = new JK.SiteValidator('twitter', userNameSuccessCallback, userNameFailCallback, parent); window.twitterValidator.init(); - window.soundCloudRecordingValidator = new JK.RecordingSourceValidator('rec_soundcloud', soundCloudSuccessCallback, siteFailCallback); + window.soundCloudRecordingValidator = new JK.RecordingSourceValidator('rec_soundcloud', soundCloudSuccessCallback, siteFailCallback, parent); logger.debug("window.soundCloudRecordingValidator", window.soundCloudRecordingValidator) - window.youTubeRecordingValidator = new JK.RecordingSourceValidator('rec_youtube', youTubeSuccessCallback, siteFailCallback); + window.youTubeRecordingValidator = new JK.RecordingSourceValidator('rec_youtube', youTubeSuccessCallback, siteFailCallback, parent); $document.triggerHandler('INIT_SITE_VALIDATORS'); }, 1); @@ -403,7 +402,7 @@ $inputDiv.find('.error-text').remove(); } - function userNameFailCallback($inputDiv) { + function userNameFailCallback($inputDiv) { $inputDiv.addClass('error'); $inputDiv.find('.error-text').remove(); $inputDiv.append("Invalid username").show(); @@ -446,9 +445,9 @@ $inputDiv.append("Invalid URL").show(); } }); - }); + //}); - $(document).on('INIT_SITE_VALIDATORS', function(e, data) { + $document.on('INIT_SITE_VALIDATORS', function(e, data) { logger.debug("ZZZwindow.soundCloudRecordingValidator", window.soundCloudRecordingValidator) window.soundCloudRecordingValidator.init(window.soundCloudRecordingSources); window.youTubeRecordingValidator.init(window.youTubeRecordingSources); diff --git a/web/app/assets/javascripts/site_validator.js.coffee b/web/app/assets/javascripts/site_validator.js.coffee index 0ce2c645a..41636af26 100644 --- a/web/app/assets/javascripts/site_validator.js.coffee +++ b/web/app/assets/javascripts/site_validator.js.coffee @@ -4,12 +4,13 @@ context.JK ||= {}; context.JK.SiteValidator = class SiteValidator - constructor: (site_type, success_callback, fail_callback) -> + constructor: (site_type, success_callback, fail_callback, parent) -> @EVENTS = context.JK.EVENTS @rest = context.JK.Rest() @site_type = site_type - @input_div = $(".site_validator#"+site_type+"_validator") - @data_input = @input_div.find('input') + @input_div = $(".site_validator."+site_type+"_validator", parent) + @data_input = @input_div.find('input') + @logger = context.JK.logger @spinner = @input_div.find('span.spinner-small') @checkmark = @input_div.find('.validate-checkmark') @@ -33,7 +34,7 @@ context.JK.SiteValidator = class SiteValidator @site_status = null dataToValidate: () => - url = @data_input.val() + url = @data_input.val() if url && 0 < url.length url.substring(0,2000) else @@ -129,8 +130,8 @@ context.JK.SiteValidator = class SiteValidator context.JK.RecordingSourceValidator = class RecordingSourceValidator extends SiteValidator - constructor: (site_type, success_callback, fail_callback) -> - super(site_type) + constructor: (site_type, success_callback, fail_callback, parent) -> + super(site_type, success_callback, fail_callback, parent) @recording_sources = [] @is_rec_src = true @add_btn = @input_div.find('a.add-recording-source') diff --git a/web/app/views/clients/_profile_online_sample_controls.html.slim b/web/app/views/clients/_profile_online_sample_controls.html.slim index 6c349835f..55f59fb39 100644 --- a/web/app/views/clients/_profile_online_sample_controls.html.slim +++ b/web/app/views/clients/_profile_online_sample_controls.html.slim @@ -7,27 +7,27 @@ td colspan="25%": label Bandcamp (username): tr td colspan="25%" - table.control-table: tr#url_validator.presence.site_validator + table.control-table: tr.url_validator.presence.site_validator td colspan="100%" - input#website maxlength="4000" type="text" + input.website maxlength="4000" type="text" td span.spinner-small td colspan="25%" - table.control-table: tr#soundcloud_validator.presence.site_validator + table.control-table: tr.soundcloud_validator.presence.site_validator td colspan="100%" - input#soundcloud-username maxlength="100" type="text" + input.soundcloud-username maxlength="100" type="text" td span.spinner-small td colspan="25%" - table.control-table: tr#reverbnation_validator.presence.site_validator + table.control-table: tr.reverbnation_validator.presence.site_validator td colspan="100%" - input#reverbnation-username maxlength="100" type="text" + input.reverbnation-username maxlength="100" type="text" td span.spinner-small td colspan="25%" - table.control-table: tr#bandcamp_validator.presence.site_validator + table.control-table: tr.bandcamp_validator.presence.site_validator td colspan="100%" - input#bandcamp-username maxlength="100" type="text" + input.bandcamp-username maxlength="100" type="text" td span.spinner-small @@ -38,27 +38,27 @@ td colspan="25%": label Twitter (username): tr td colspan="25%" - table.control-table: tr#fandalism_validator.presence.site_validator + table.control-table: tr.fandalism_validator.presence.site_validator td colspan="100%" - input#fandalism-username maxlength="100" type="text" + input.fandalism-username maxlength="100" type="text" td span.spinner-small td colspan="25%" - table.control-table: tr#youtube_validator.presence.site_validator + table.control-table: tr.youtube_validator.presence.site_validator td colspan="100%" - input#youtube-username maxlength="100" type="text" + input.youtube-username maxlength="100" type="text" td span.spinner-small td colspan="25%" - table.control-table: tr#facebook_validator.presence.site_validator + table.control-table: tr.facebook_validator.presence.site_validator td colspan="100%" - input#facebook-username maxlength="100" type="text" + input.facebook-username maxlength="100" type="text" td span.spinner-small td colspan="25%" - table.control-table: tr#twitter_validator.presence.site_validator + table.control-table: tr.twitter_validator.presence.site_validator td colspan="100%" - input#twitter-username maxlength="100" type="text" + input.twitter-username maxlength="100" type="text" td span.spinner-small tr @@ -67,20 +67,20 @@ td colspan="33.33%": label YouTube Videos (URL): tr td colspan="33.33%" - a#btn-add-jk-recording.button-grey BROWSE + a.btn-add-jk-recording.button-grey BROWSE td colspan="33.33%" - table.control-table: tr#rec_soundcloud_validator.sample.site_validator + table.control-table: tr.rec_soundcloud_validator.sample.site_validator td colspan="100%" - input#soundcloud-recording maxlength="4000" type="text" + input.soundcloud-recording maxlength="4000" type="text" td - a#btn-add-soundcloud-recording.button-grey.add-recording-source ADD + a.btn-add-soundcloud-recording.button-grey.add-recording-source ADD span.spinner-small td colspan="33.33%" - table.control-table: tr#rec_youtube_validator.sample.site_validator + table.control-table: tr.rec_youtube_validator.sample.site_validator td colspan="100%" - input#youtube-video maxlength="4000" type="text" + input.youtube-video maxlength="4000" type="text" td - a#btn-add-youtube-video.button-grey.add-recording-source ADD + a.btn-add-youtube-video.button-grey.add-recording-source ADD span.spinner-small tr td colspan="33.33%": .sample-list From 6c5ffd66445a3eaa96fee3a000f9305910358061 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Wed, 3 Jun 2015 16:34:37 -0500 Subject: [PATCH 174/195] VRFS-3246 : Cleanup code and style --- ruby/lib/jam_ruby/models/band.rb | 3 +- ruby/lib/jam_ruby/models/online_presence.rb | 4 +- web/app/assets/javascripts/band_setup.js | 45 +++++++++++++------ .../client/accountProfileSamples.css.scss | 3 +- 4 files changed, 38 insertions(+), 17 deletions(-) diff --git a/ruby/lib/jam_ruby/models/band.rb b/ruby/lib/jam_ruby/models/band.rb index 6d6c6f96e..eeb71ad59 100644 --- a/ruby/lib/jam_ruby/models/band.rb +++ b/ruby/lib/jam_ruby/models/band.rb @@ -226,8 +226,7 @@ module JamRuby end # helper method for creating / updating a Band - def self.save(user, params) - puts "Band.save with #{user}, #{params}" + def self.save(user, params) band = build_band(user, params) if band.save diff --git a/ruby/lib/jam_ruby/models/online_presence.rb b/ruby/lib/jam_ruby/models/online_presence.rb index 6f97baa7f..ed4f498bd 100644 --- a/ruby/lib/jam_ruby/models/online_presence.rb +++ b/ruby/lib/jam_ruby/models/online_presence.rb @@ -62,7 +62,9 @@ module JamRuby private def self.auth_player(target_player, options={}) - raise JamPermissionError, PERMISSION_MSG if target_player.nil? || options[:player_id] != target_player.id + if target_player.nil? || options[:player_id] != target_player.id + raise JamPermissionError, PERMISSION_MSG + end end end end \ No newline at end of file diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 5e0e71222..15524566d 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -37,11 +37,9 @@ var $screen=$("#band-setup") var $samples = $screen.find(".account-profile-samples") var $selectedInstruments=[] - - console.log("------------------------------------------") + var accountProfileSamples = new JK.AccountProfileSamples(app, $screen, loadBandCallback, rest.updateBand) - accountProfileSamples.initialize() - console.log("==========================================") + accountProfileSamples.initialize() function navBack() { if (currentStep>0) { @@ -245,9 +243,37 @@ band.instruments.push(h) }) + if(!isNewBand()) { + mergePerformanceSamples(band) + } + return band; } + function mergePerformanceSamples(band) { + // Collect and merge data from this sub-widget: + var performanceSampleData = accountProfileSamples.buildPlayer() + band.website=performanceSampleData.website + band.online_presences=performanceSampleData.online_presences + band.performance_samples=performanceSampleData.performance_samples + + // Change player id to that of band. Widget currently hardwires current user id: + if(band.online_presences) { + for (var i=0; i Date: Wed, 3 Jun 2015 17:23:11 -0500 Subject: [PATCH 175/195] Fix wayward merge. --- db/manifest | 1 - 1 file changed, 1 deletion(-) diff --git a/db/manifest b/db/manifest index 0d4918f5b..4ca6b3433 100755 --- a/db/manifest +++ b/db/manifest @@ -290,7 +290,6 @@ jam_track_right_private_key.sql first_downloaded_jamtrack_at.sql signing.sql optimized_redeemption.sql -optimized_redeemption.sql optimized_redemption_warn_mode.sql affiliate_partners2.sql enhance_band_profile.sql \ No newline at end of file From b39c314e5b77db62bf72fd06059621d869acf58f Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Wed, 3 Jun 2015 17:44:17 -0500 Subject: [PATCH 176/195] VRFS-3246 : Scope the unstopped selector. --- web/app/assets/javascripts/band_setup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/assets/javascripts/band_setup.js b/web/app/assets/javascripts/band_setup.js index 15524566d..9d4b59487 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -719,7 +719,7 @@ app.bindScreen('band/setup', screenBindings) - $('input[type=radio]').iCheck({ + $screen.find('input[type=radio]').iCheck({ checkboxClass: 'icheckbox_minimal', radioClass: 'iradio_minimal', inheritClass: true From 972b65c4efbb6eebecfc0d5a94c7cc6ba0be0375 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Thu, 4 Jun 2015 14:05:20 -0500 Subject: [PATCH 177/195] VRFS-3190 : Fix a few functional and style problems. Cleanup as necessary. --- .../javascripts/accounts_profile_samples.js | 50 +++++++++++-------- .../client/accountProfileSamples.css.scss | 10 ++++ .../_profile_online_sample_controls.html.slim | 16 ++++-- web/app/views/clients/index.html.erb | 2 +- 4 files changed, 50 insertions(+), 28 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 30cd85c20..4ba0afd28 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -32,9 +32,9 @@ var $twitterUsername = $screen.find('.twitter-username'); // performance samples - var $jamkazamSampleList = $screen.find('.samples.jamkazam').find('.sample-list'); - var $soundCloudSampleList = $screen.find('.samples.soundcloud').find('.sample-list'); - var $youTubeSampleList = $screen.find('.samples.youtube').find('.sample-list'); + var $jamkazamSampleList = $screen.find(".sample-list[source-type='jamkazam']") + var $soundCloudSampleList = $screen.find(".sample-list[source-type='soundcloud']") + var $youTubeSampleList = $screen.find(".sample-list[source-type='youtube']") // buttons var $btnAddJkRecording = $screen.find('.btn-add-jk-recording'); @@ -51,7 +51,7 @@ if (targetPlayer && targetPlayer.keys && targetPlayer.keys.length > 0) { renderPlayer(targetPlayer) } - }); + }) } function renderPlayer(targetPlayer) { @@ -120,7 +120,7 @@ } function loadSamples(samples, type, $sampleList, recordingSources) { - $sampleList.empty(); + $sampleList.empty(); if (type === 'jamkazam') { $.each(samples, function(index, val) { @@ -131,8 +131,7 @@ buildJamkazamEntry(val.claimed_recording.id, val.claimed_recording.name); }); - } - else { + } else { if (samples && samples.length > 0) { $.each(samples, function(index, val) { @@ -208,6 +207,9 @@ }); enableSubmits(); + + $(".sample-list .close-button", $screen).on("click", function(e) { + } } function enableSubmits() { @@ -257,9 +259,7 @@ 'service_type': type, 'claimed_recording_id': id, }); - } - - else { + } else { var url = $(this).attr('data-recording-url'); var title = $(this).attr('data-recording-title'); @@ -325,14 +325,19 @@ if(xhr.status == 422) { - } - else { + } else { app.ajaxError(xhr, textStatus, errorMessage) } } function removeRow(recordingId, type) { $('div[data-recording-id=' + recordingId + ']').remove(); + var sampleList = $('.sample-list[source-type="' + type + '"]') + var rowCnt = sampleList.find('.recording-row').length + + if (rowCnt==0) { + sampleList.find(".empty").removeClass("hidden") + } if (type === 'soundcloud') { window.soundCloudRecordingValidator.removeRecordingId(recordingId); @@ -361,10 +366,11 @@ //var $screen = $('#account-profile-samples'); var $btnAddSoundCloudRecording = $screen.find('.btn-add-soundcloud-recording'); var $btnAddYouTubeVideo = $screen.find('.btn-add-youtube-video'); - var $soundCloudSampleList = $screen.find('.samples.soundcloud'); - var $youTubeSampleList = $screen.find('.samples.youtube'); + // var $soundCloudSampleList = $screen.find('.samples.soundcloud'); + // var $youTubeSampleList = $screen.find('.samples.youtube'); + - setTimeout(function() { + setTimeout(function() { window.urlValidator = new JK.SiteValidator('url', userNameSuccessCallback, userNameFailCallback, parent); window.urlValidator.init(); @@ -390,7 +396,7 @@ window.twitterValidator.init(); window.soundCloudRecordingValidator = new JK.RecordingSourceValidator('rec_soundcloud', soundCloudSuccessCallback, siteFailCallback, parent); - logger.debug("window.soundCloudRecordingValidator", window.soundCloudRecordingValidator) + logger.debug("window.soundCloudRecordingValidator", window.soundCloudRecordingValidator, parent) window.youTubeRecordingValidator = new JK.RecordingSourceValidator('rec_youtube', youTubeSuccessCallback, siteFailCallback, parent); $document.triggerHandler('INIT_SITE_VALIDATORS'); @@ -408,7 +414,7 @@ $inputDiv.append("Invalid username").show(); } - function soundCloudSuccessCallback($inputDiv) { + function soundCloudSuccessCallback($inputDiv) { siteSuccessCallback($inputDiv, window.soundCloudRecordingValidator, $soundCloudSampleList, 'soundcloud'); } @@ -416,24 +422,24 @@ siteSuccessCallback($inputDiv, window.youTubeRecordingValidator, $youTubeSampleList, 'youtube'); } - function siteSuccessCallback($inputDiv, recordingSiteValidator, $sampleList, type) { + function siteSuccessCallback($inputDiv, recordingSiteValidator, sampleList, type) { + sampleList.find(".empty").addClass("hidden") $inputDiv.removeClass('error'); $inputDiv.find('.error-text').remove(); var recordingSources = recordingSiteValidator.recordingSources(); if (recordingSources && recordingSources.length > 0) { - var $sampleList = $sampleList.find('.sample-list'); var addedRecording = recordingSources[recordingSources.length-1]; - // TODO: this code is repeated in JS file + // TODO: this code is repeated in elsewhere in this JS file: var recordingIdAttr = ' data-recording-id="' + addedRecording.recording_id + '" '; var recordingUrlAttr = ' data-recording-url="' + addedRecording.url + '" '; var recordingTitleAttr = ' data-recording-title="' + addedRecording.recording_title + '"'; var title = formatTitle(addedRecording.recording_title); - $sampleList.append('
    ' + title + '
    '); + sampleList.append('
    ' + title + '
    '); var onclick = "onclick=removeRow(\'" + addedRecording.recording_id + "\',\'" + type + "\');"; - $sampleList.append('
    X
    '); + sampleList.append('
    X
    '); } $inputDiv.find('input').val(''); diff --git a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss index a6f641d57..629ebc3f1 100644 --- a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss +++ b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss @@ -14,6 +14,16 @@ } } + .sample-list { + border: 1px inset #cfcfcf; + padding: 0.5em; + .empty { + font-style: italic; + } + min-height: 150px; + overflow: scroll; + } + table.control-table { width: 100%; } diff --git a/web/app/views/clients/_profile_online_sample_controls.html.slim b/web/app/views/clients/_profile_online_sample_controls.html.slim index 55f59fb39..67e57337d 100644 --- a/web/app/views/clients/_profile_online_sample_controls.html.slim +++ b/web/app/views/clients/_profile_online_sample_controls.html.slim @@ -65,7 +65,7 @@ td colspan="33.33%": label JamKazam Recordings: td colspan="33.33%": label SoundCloud Recordings (URL): td colspan="33.33%": label YouTube Videos (URL): - tr + tr.add-samples-controls-row td colspan="33.33%" a.btn-add-jk-recording.button-grey BROWSE td colspan="33.33%" @@ -82,7 +82,13 @@ td a.btn-add-youtube-video.button-grey.add-recording-source ADD span.spinner-small - tr - td colspan="33.33%": .sample-list - td colspan="33.33%": .sample-list - td colspan="33.33%": .sample-list \ No newline at end of file + tr.add-samples-list-row + td colspan="33.33%" + .sample-list source-type='jamkazam' + .empty No recordings + td colspan="33.33%" + .sample-list source-type='soundcloud' + .empty No recordings + td colspan="33.33%" + .sample-list source-type='youtube' + .empty No recordings \ No newline at end of file diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index c41ae70bc..09aab62fa 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -236,7 +236,7 @@ var accountProfileInterests = new JK.AccountProfileInterests(JK.app); accountProfileInterests.initialize(); - var accountProfileSamples = new JK.AccountProfileSamples(JK.app, null) + var accountProfileSamples = new JK.AccountProfileSamples(JK.app, $(".account-profile-samples")) accountProfileSamples.initialize(); var accountAudioProfile = new JK.AccountAudioProfile(JK.app); From cb058e851b1a1f9900c12a0428e9ada143b066f3 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Thu, 4 Jun 2015 18:55:15 -0500 Subject: [PATCH 178/195] VRFS-3190 : Several bug fixes for band profile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Storing validator in window was screwing things up now that we have multiple of these. This is now stored at the class instance level. * Deleting a performance sample didn’t delete it from the validator, as the comparison was flaky. * Styling fixes --- .../javascripts/accounts_profile_samples.js | 151 +++++++++--------- .../javascripts/site_validator.js.coffee | 39 ++--- .../client/accountProfileSamples.css.scss | 13 +- 3 files changed, 105 insertions(+), 98 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 4ba0afd28..b2b34132a 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -5,14 +5,14 @@ context.JK = context.JK || {}; // TODO: Add a target type, which can be band or user -- call the - // appropriate API methods. + // appropriate API methods. context.JK.AccountProfileSamples = function(app, parent, loadFn, updateFn) { var $document = $(document) // used to initialize RecordingSourceValidator in site_validator.js.coffee - window.jamkazamRecordingSources = []; - window.soundCloudRecordingSources = []; - window.youTubeRecordingSources = []; + var jamkazamRecordingSources = []; + var soundCloudRecordingSources = []; + var youTubeRecordingSources = []; var logger = context.JK.logger; var EVENTS = context.JK.EVENTS; @@ -20,9 +20,9 @@ var ui = new context.JK.UIHelper(JK.app); var target = {}; var profileUtils = context.JK.ProfileUtils; - var $screen = $('.profile-online-sample-controls', parent); + var $screen = $('.profile-online-sample-controls', parent); // online presences - var $website = $screen.find('.website'); + var $website = $screen.find('.website'); var $soundCloudUsername = $screen.find('.soundcloud-username'); var $reverbNationUsername = $screen.find('.reverbnation-username'); var $bandCampUsername = $screen.find('.bandcamp-username'); @@ -42,10 +42,22 @@ var $btnBack = $screen.find('.account-edit-profile-back'); var $btnSubmit = $screen.find('.account-edit-profile-submit'); + + var urlValidator=null; + var soundCloudValidator=null; + var reverbNationValidator=null; + var bandCampValidator=null; + var fandalismValidator=null; + var youTubeValidator=null; + var facebookValidator=null; + var twitterValidator=null; + var soundCloudRecordingValidator=null; + var youTubeRecordingValidator=null; + function beforeShow(data) { } - function afterShow(data) { + function afterShow(data) { $.when(loadFn()) .done(function(targetPlayer) { if (targetPlayer && targetPlayer.keys && targetPlayer.keys.length > 0) { @@ -56,7 +68,7 @@ function renderPlayer(targetPlayer) { renderPresence(targetPlayer); - renderSamples(targetPlayer); + renderSamples(targetPlayer); } function renderPresence(targetPlayer) { @@ -108,20 +120,20 @@ function renderSamples(targetPlayer) { // JamKazam recordings var samples = profileUtils.jamkazamSamples(targetPlayer.performance_samples); - loadSamples(samples, 'jamkazam', $jamkazamSampleList, window.jamkazamRecordingSources); + loadSamples(samples, 'jamkazam', $jamkazamSampleList, jamkazamRecordingSources); // SoundCloud recordings samples = profileUtils.soundCloudSamples(targetPlayer.performance_samples); - loadSamples(samples, 'soundcloud', $soundCloudSampleList, window.soundCloudRecordingSources); + loadSamples(samples, 'soundcloud', $soundCloudSampleList, soundCloudRecordingSources); // YouTube videos samples = profileUtils.youTubeSamples(targetPlayer.performance_samples); - loadSamples(samples, 'youtube', $youTubeSampleList, window.youTubeRecordingSources); + loadSamples(samples, 'youtube', $youTubeSampleList, youTubeRecordingSources); } function loadSamples(samples, type, $sampleList, recordingSources) { - $sampleList.empty(); - + $sampleList.find(":not(.empty)").remove(); + if (type === 'jamkazam') { $.each(samples, function(index, val) { recordingSources.push({ @@ -147,9 +159,7 @@ var recordingTitleAttr = ' data-recording-title="' + val.description + '"'; var title = formatTitle(val.description); $sampleList.append('
    ' + title + '
    '); - - var onclick = "onclick=removeRow(\'" + val.service_id + "\',\'" + type + "\');"; - $sampleList.append('
    X
    '); + $sampleList.append('
    X
    '); }); } } @@ -160,9 +170,7 @@ var recordingIdAttr = ' data-recording-id="' + recordingId + '" '; $jamkazamSampleList.append('
    ' + title + '
    '); - - var onclick = "onclick=removeRow(\'" + recordingId + "\',\'jamkazam\');"; - $jamkazamSampleList.append('
    X
    '); + $jamkazamSampleList.append('
    X
    '); } function events() { @@ -174,14 +182,14 @@ // retrieve recordings and pass to modal dialog api.getClaimedRecordings() .done(function(response) { - ui.launchRecordingSelectorDialog(response, window.jamkazamRecordingSources, function(selectedRecordings) { + ui.launchRecordingSelectorDialog(response, jamkazamRecordingSources, function(selectedRecordings) { $jamkazamSampleList.empty(); - window.jamkazamRecordingSources = []; + jamkazamRecordingSources = []; // update the list with the selected recordings $.each(selectedRecordings, function(index, val) { - window.jamkazamRecordingSources.push({ + jamkazamRecordingSources.push({ 'claimed_recording_id': val.id, 'description': val.name }); @@ -190,7 +198,7 @@ }); }); }); - + return false; }); @@ -208,8 +216,9 @@ enableSubmits(); - $(".sample-list .close-button", $screen).on("click", function(e) { - } + $screen.find(".sample-list").off("click").on("click", ".close-button", function(e) { + removeRow($(this).data("recording-id"), $(this).data("recording-type")) + }) } function enableSubmits() { @@ -291,7 +300,7 @@ function buildPlayer() { // extract online presences var op = []; - var presenceTypes = profileUtils.ONLINE_PRESENCE_TYPES; + var presenceTypes = profileUtils.ONLINE_PRESENCE_TYPES; addOnlinePresence(op, $soundCloudUsername.val(), presenceTypes.SOUNDCLOUD.description); addOnlinePresence(op, $reverbNationUsername.val(), presenceTypes.REVERBNATION.description); addOnlinePresence(op, $bandCampUsername.val(), presenceTypes.BANDCAMP.description); @@ -306,12 +315,12 @@ addPerformanceSamples(ps, $jamkazamSampleList, performanceSampleTypes.JAMKAZAM.description); addPerformanceSamples(ps, $soundCloudSampleList, performanceSampleTypes.SOUNDCLOUD.description); addPerformanceSamples(ps, $youTubeSampleList, performanceSampleTypes.YOUTUBE.description); - + return { website: $website.val(), online_presences: op, performance_samples: ps - } + } } function postUpdateProfileSuccess(response) { @@ -334,16 +343,14 @@ $('div[data-recording-id=' + recordingId + ']').remove(); var sampleList = $('.sample-list[source-type="' + type + '"]') var rowCnt = sampleList.find('.recording-row').length - - if (rowCnt==0) { + if (0==parseInt(rowCnt)) { sampleList.find(".empty").removeClass("hidden") } if (type === 'soundcloud') { - window.soundCloudRecordingValidator.removeRecordingId(recordingId); - } - else if (type === 'youtube') { - window.youTubeRecordingValidator.removeRecordingId(recordingId); + soundCloudRecordingValidator.removeRecordingId(recordingId); + } else if (type === 'youtube') { + youTubeRecordingValidator.removeRecordingId(recordingId); } } @@ -352,13 +359,13 @@ } // This function is a bit of a mess. It was pulled - // from the html.erb file verbatim, and could use a + // from the html.erb file verbatim, and could use a // refactor: - function initializeValidators() { + function initializeValidators() { var initialized = false; - //$document.on('JAMKAZAM_READY', function(e, data) { - window.JK.JamServer.get$Server().on(window.JK.EVENTS.CONNECTION_UP, function() { - if(initialized) { + //$document.on('JAMKAZAM_READY', function(e, data) { + JK.JamServer.get$Server().on(JK.EVENTS.CONNECTION_UP, function() { + if(initialized) { return; } initialized = true; @@ -368,36 +375,35 @@ var $btnAddYouTubeVideo = $screen.find('.btn-add-youtube-video'); // var $soundCloudSampleList = $screen.find('.samples.soundcloud'); // var $youTubeSampleList = $screen.find('.samples.youtube'); - - setTimeout(function() { - window.urlValidator = new JK.SiteValidator('url', userNameSuccessCallback, userNameFailCallback, parent); - window.urlValidator.init(); - window.soundCloudValidator = new JK.SiteValidator('soundcloud', userNameSuccessCallback, userNameFailCallback, parent); - window.soundCloudValidator.init(); + setTimeout(function() { + urlValidator = new JK.SiteValidator('url', userNameSuccessCallback, userNameFailCallback, parent); + urlValidator.init(); - window.reverbNationValidator = new JK.SiteValidator('reverbnation', userNameSuccessCallback, userNameFailCallback, parent); - window.reverbNationValidator.init(); + soundCloudValidator = new JK.SiteValidator('soundcloud', userNameSuccessCallback, userNameFailCallback, parent); + soundCloudValidator.init(); - window.bandCampValidator = new JK.SiteValidator('bandcamp', userNameSuccessCallback, userNameFailCallback, parent); - window.bandCampValidator.init(); + reverbNationValidator = new JK.SiteValidator('reverbnation', userNameSuccessCallback, userNameFailCallback, parent); + reverbNationValidator.init(); - window.fandalismValidator = new JK.SiteValidator('fandalism', userNameSuccessCallback, userNameFailCallback, parent); - window.fandalismValidator.init(); + bandCampValidator = new JK.SiteValidator('bandcamp', userNameSuccessCallback, userNameFailCallback, parent); + bandCampValidator.init(); - window.youTubeValidator = new JK.SiteValidator('youtube', userNameSuccessCallback, userNameFailCallback, parent); - window.youTubeValidator.init(); + fandalismValidator = new JK.SiteValidator('fandalism', userNameSuccessCallback, userNameFailCallback, parent); + fandalismValidator.init(); - window.facebookValidator = new JK.SiteValidator('facebook', userNameSuccessCallback, userNameFailCallback, parent); - window.facebookValidator.init(); + youTubeValidator = new JK.SiteValidator('youtube', userNameSuccessCallback, userNameFailCallback, parent); + youTubeValidator.init(); - window.twitterValidator = new JK.SiteValidator('twitter', userNameSuccessCallback, userNameFailCallback, parent); - window.twitterValidator.init(); + facebookValidator = new JK.SiteValidator('facebook', userNameSuccessCallback, userNameFailCallback, parent); + facebookValidator.init(); - window.soundCloudRecordingValidator = new JK.RecordingSourceValidator('rec_soundcloud', soundCloudSuccessCallback, siteFailCallback, parent); - logger.debug("window.soundCloudRecordingValidator", window.soundCloudRecordingValidator, parent) - window.youTubeRecordingValidator = new JK.RecordingSourceValidator('rec_youtube', youTubeSuccessCallback, siteFailCallback, parent); + twitterValidator = new JK.SiteValidator('twitter', userNameSuccessCallback, userNameFailCallback, parent); + twitterValidator.init(); + + soundCloudRecordingValidator = new JK.RecordingSourceValidator('rec_soundcloud', soundCloudSuccessCallback, siteFailCallback, parent); + youTubeRecordingValidator = new JK.RecordingSourceValidator('rec_youtube', youTubeSuccessCallback, siteFailCallback, parent); $document.triggerHandler('INIT_SITE_VALIDATORS'); }, 1); @@ -408,21 +414,21 @@ $inputDiv.find('.error-text').remove(); } - function userNameFailCallback($inputDiv) { + function userNameFailCallback($inputDiv) { $inputDiv.addClass('error'); $inputDiv.find('.error-text').remove(); $inputDiv.append("Invalid username").show(); } - function soundCloudSuccessCallback($inputDiv) { - siteSuccessCallback($inputDiv, window.soundCloudRecordingValidator, $soundCloudSampleList, 'soundcloud'); + function soundCloudSuccessCallback($inputDiv) { + siteSuccessCallback($inputDiv, soundCloudRecordingValidator, $soundCloudSampleList, 'soundcloud'); } function youTubeSuccessCallback($inputDiv) { - siteSuccessCallback($inputDiv, window.youTubeRecordingValidator, $youTubeSampleList, 'youtube'); + siteSuccessCallback($inputDiv, youTubeRecordingValidator, $youTubeSampleList, 'youtube'); } - function siteSuccessCallback($inputDiv, recordingSiteValidator, sampleList, type) { + function siteSuccessCallback($inputDiv, recordingSiteValidator, sampleList, type) { sampleList.find(".empty").addClass("hidden") $inputDiv.removeClass('error'); $inputDiv.find('.error-text').remove(); @@ -437,9 +443,7 @@ var recordingTitleAttr = ' data-recording-title="' + addedRecording.recording_title + '"'; var title = formatTitle(addedRecording.recording_title); sampleList.append('
    ' + title + '
    '); - - var onclick = "onclick=removeRow(\'" + addedRecording.recording_id + "\',\'" + type + "\');"; - sampleList.append('
    X
    '); + sampleList.append('
    X
    '); } $inputDiv.find('input').val(''); @@ -454,16 +458,15 @@ //}); $document.on('INIT_SITE_VALIDATORS', function(e, data) { - logger.debug("ZZZwindow.soundCloudRecordingValidator", window.soundCloudRecordingValidator) - window.soundCloudRecordingValidator.init(window.soundCloudRecordingSources); - window.youTubeRecordingValidator.init(window.youTubeRecordingSources); + soundCloudRecordingValidator.init(soundCloudRecordingSources); + youTubeRecordingValidator.init(youTubeRecordingSources); }); - - } // end initializeValidators. + + } // end initializeValidators. function resetForm() { - $("input", $screen).val("") + $("input", $screen).val("") } function initialize() { diff --git a/web/app/assets/javascripts/site_validator.js.coffee b/web/app/assets/javascripts/site_validator.js.coffee index 41636af26..111457aff 100644 --- a/web/app/assets/javascripts/site_validator.js.coffee +++ b/web/app/assets/javascripts/site_validator.js.coffee @@ -9,8 +9,8 @@ context.JK.SiteValidator = class SiteValidator @rest = context.JK.Rest() @site_type = site_type @input_div = $(".site_validator."+site_type+"_validator", parent) - @data_input = @input_div.find('input') - + @data_input = @input_div.find('input') + @logger = context.JK.logger @spinner = @input_div.find('span.spinner-small') @checkmark = @input_div.find('.validate-checkmark') @@ -34,7 +34,7 @@ context.JK.SiteValidator = class SiteValidator @site_status = null dataToValidate: () => - url = @data_input.val() + url = @data_input.val() if url && 0 < url.length url.substring(0,2000) else @@ -50,7 +50,7 @@ context.JK.SiteValidator = class SiteValidator @checkmark.hide() yn - didBlur: () => + didBlur: () => if this.showFormatStatus() this.validateSite() @@ -100,14 +100,14 @@ context.JK.SiteValidator = class SiteValidator @checkmark.show() else @checkmark.hide() - - siteIsValid: () => + + siteIsValid: () => this.setSiteStatus(true) - - siteIsInvalid: () => + + siteIsInvalid: () => this.setSiteStatus(false) - - renderErrors: (errors) => + + renderErrors: (errors) => errdiv = @input_div.find('.error') if errmsg = context.JK.format_errors("site", errors) errdiv.show() @@ -120,7 +120,7 @@ context.JK.SiteValidator = class SiteValidator dfr = $.Deferred() if null == @site_status @deferred_status_check = dfr - this.validateSite() + this.validateSite() else if true == @site_status dfr.resolve() @@ -128,9 +128,9 @@ context.JK.SiteValidator = class SiteValidator dfr.reject() return dfr.promise() - -context.JK.RecordingSourceValidator = class RecordingSourceValidator extends SiteValidator - constructor: (site_type, success_callback, fail_callback, parent) -> + +context.JK.RecordingSourceValidator = class RecordingSourceValidator extends SiteValidator + constructor: (site_type, success_callback, fail_callback, parent) -> super(site_type, success_callback, fail_callback, parent) @recording_sources = [] @is_rec_src = true @@ -142,7 +142,7 @@ context.JK.RecordingSourceValidator = class RecordingSourceValidator extends Sit super() if sources @recording_sources = sources - @add_btn.on 'click', => + @add_btn.off('click').on 'click', => this.attemptAdd() processSiteCheckSucceed: (response) => @@ -163,7 +163,7 @@ context.JK.RecordingSourceValidator = class RecordingSourceValidator extends Sit if @site_fail_callback @site_fail_callback(@input_div) - didBlur: () => + didBlur: () => # do nothing, validate on add only validateSite: () => @@ -177,13 +177,14 @@ context.JK.RecordingSourceValidator = class RecordingSourceValidator extends Sit removeRecordingId: (recording_id) => start_len = @recording_sources.length - @recording_sources = $.grep @recording_sources, (src_data) -> - src_data['recording_id'] != recording_id + @recording_sources = @recording_sources.filter (src) -> + src["recording_id"] isnt recording_id.toString() start_len != @recording_sources.length containsRecordingUrl: (url) => vals = $.grep @recording_sources, (src_data) -> - src_data['url'] == url + src_data['url'] is url + 0 < vals.length recordingSources: () => diff --git a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss index 629ebc3f1..76c76ebc8 100644 --- a/web/app/assets/stylesheets/client/accountProfileSamples.css.scss +++ b/web/app/assets/stylesheets/client/accountProfileSamples.css.scss @@ -1,11 +1,11 @@ @import "common.css.scss"; @import "site_validator.css.scss"; -.profile-online-sample-controls { +.profile-online-sample-controls { table.profile-table { width: 100%; tr:nth-child(even) td { - padding: 0.25em 0.25em 1em 0.25em; + padding: 0.25em 0.25em 1em 0.25em; vertical-align: top; } tr:nth-child(odd) td { @@ -22,12 +22,15 @@ } min-height: 150px; overflow: scroll; + .close-button { + cursor:pointer; + } } table.control-table { width: 100%; } - + .sample-row { position: relative; clear: both; @@ -39,12 +42,12 @@ } .presence { - margin: 3px 30px 15px 0px; + margin: 3px 30px 15px 0px; } .site_validator { a, .spinner-small { - margin: 1px 1px 2px 2px; + margin: 1px 1px 2px 2px; vertical-align: top; } } From 61268bdc309328e7d959042133e6c31ba41c3bd6 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Fri, 5 Jun 2015 09:24:49 -0500 Subject: [PATCH 179/195] VRFS-3190 : Fix race condition. Init should be called in the same asynchronous block as construction. --- .../javascripts/accounts_profile_samples.js | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index b2b34132a..67fc330be 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -378,36 +378,36 @@ setTimeout(function() { - urlValidator = new JK.SiteValidator('url', userNameSuccessCallback, userNameFailCallback, parent); - urlValidator.init(); + urlValidator = new JK.SiteValidator('url', userNameSuccessCallback, userNameFailCallback, parent) + urlValidator.init() - soundCloudValidator = new JK.SiteValidator('soundcloud', userNameSuccessCallback, userNameFailCallback, parent); - soundCloudValidator.init(); + soundCloudValidator = new JK.SiteValidator('soundcloud', userNameSuccessCallback, userNameFailCallback, parent) + soundCloudValidator.init() - reverbNationValidator = new JK.SiteValidator('reverbnation', userNameSuccessCallback, userNameFailCallback, parent); - reverbNationValidator.init(); + reverbNationValidator = new JK.SiteValidator('reverbnation', userNameSuccessCallback, userNameFailCallback, parent) + reverbNationValidator.init() - bandCampValidator = new JK.SiteValidator('bandcamp', userNameSuccessCallback, userNameFailCallback, parent); - bandCampValidator.init(); + bandCampValidator = new JK.SiteValidator('bandcamp', userNameSuccessCallback, userNameFailCallback, parent) + bandCampValidator.init() - fandalismValidator = new JK.SiteValidator('fandalism', userNameSuccessCallback, userNameFailCallback, parent); - fandalismValidator.init(); + fandalismValidator = new JK.SiteValidator('fandalism', userNameSuccessCallback, userNameFailCallback, parent) + fandalismValidator.init() - youTubeValidator = new JK.SiteValidator('youtube', userNameSuccessCallback, userNameFailCallback, parent); - youTubeValidator.init(); + youTubeValidator = new JK.SiteValidator('youtube', userNameSuccessCallback, userNameFailCallback, parent) + youTubeValidator.init() - facebookValidator = new JK.SiteValidator('facebook', userNameSuccessCallback, userNameFailCallback, parent); - facebookValidator.init(); + facebookValidator = new JK.SiteValidator('facebook', userNameSuccessCallback, userNameFailCallback, parent) + facebookValidator.init() - twitterValidator = new JK.SiteValidator('twitter', userNameSuccessCallback, userNameFailCallback, parent); - twitterValidator.init(); + twitterValidator = new JK.SiteValidator('twitter', userNameSuccessCallback, userNameFailCallback, parent) + twitterValidator.init() - soundCloudRecordingValidator = new JK.RecordingSourceValidator('rec_soundcloud', soundCloudSuccessCallback, siteFailCallback, parent); - youTubeRecordingValidator = new JK.RecordingSourceValidator('rec_youtube', youTubeSuccessCallback, siteFailCallback, parent); - - $document.triggerHandler('INIT_SITE_VALIDATORS'); - }, 1); + soundCloudRecordingValidator = new JK.RecordingSourceValidator('rec_soundcloud', soundCloudSuccessCallback, siteFailCallback, parent) + youTubeRecordingValidator = new JK.RecordingSourceValidator('rec_youtube', youTubeSuccessCallback, siteFailCallback, parent) + soundCloudRecordingValidator.init(soundCloudRecordingSources) + youTubeRecordingValidator.init(youTubeRecordingSources) + }, 1) function userNameSuccessCallback($inputDiv) { $inputDiv.removeClass('error'); @@ -457,11 +457,7 @@ }); //}); - $document.on('INIT_SITE_VALIDATORS', function(e, data) { - soundCloudRecordingValidator.init(soundCloudRecordingSources); - youTubeRecordingValidator.init(youTubeRecordingSources); - }); - + } // end initializeValidators. From 01c3b8af5fa76237746f095098007feb755dcd53 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Fri, 5 Jun 2015 12:55:55 -0500 Subject: [PATCH 180/195] VRFS-3190 : Use scoped classes instead of ids for navigation buttons. Initialize musician profile samples properly. --- .../assets/javascripts/accounts_profile.js | 4 +-- .../accounts_profile_experience.js | 6 ++-- .../javascripts/accounts_profile_interests.js | 6 ++-- .../javascripts/accounts_profile_samples.js | 36 +++++++++---------- web/app/views/clients/_account.html.erb | 4 +-- .../clients/_account_profile_samples.html.erb | 6 ++-- web/app/views/clients/index.html.erb | 4 +-- web/spec/features/account_spec.rb | 6 ++-- 8 files changed, 36 insertions(+), 36 deletions(-) diff --git a/web/app/assets/javascripts/accounts_profile.js b/web/app/assets/javascripts/accounts_profile.js index 584fb6ff8..9938d4cf0 100644 --- a/web/app/assets/javascripts/accounts_profile.js +++ b/web/app/assets/javascripts/accounts_profile.js @@ -28,8 +28,8 @@ var $biography = $screen.find('#biography'); var $subscribe = $screen.find('#subscribe'); - var $btnCancel = $screen.find('#account-edit-profile-cancel'); - var $btnSubmit = $screen.find('#account-edit-profile-submit'); + var $btnCancel = $screen.find('.account-edit-profile-cancel'); + var $btnSubmit = $screen.find('.account-edit-profile-submit'); function beforeShow(data) { userId = data.id; diff --git a/web/app/assets/javascripts/accounts_profile_experience.js b/web/app/assets/javascripts/accounts_profile_experience.js index 6bb22fbab..de57e8695 100644 --- a/web/app/assets/javascripts/accounts_profile_experience.js +++ b/web/app/assets/javascripts/accounts_profile_experience.js @@ -10,9 +10,9 @@ var api = context.JK.Rest(); var $screen = $('#account-profile-experience'); var profileUtils = context.JK.ProfileUtils; - var $btnCancel = $screen.find('#account-edit-profile-cancel'); - var $btnBack = $screen.find('#account-edit-profile-back'); - var $btnSubmit = $screen.find('#account-edit-profile-submit'); + var $btnCancel = $screen.find('.account-edit-profile-cancel'); + var $btnBack = $screen.find('.account-edit-profile-back'); + var $btnSubmit = $screen.find('.account-edit-profile-submit'); var $instrumentSelector = $screen.find('.instrument_selector'); var $userGenres = $screen.find('#user-genres'); diff --git a/web/app/assets/javascripts/accounts_profile_interests.js b/web/app/assets/javascripts/accounts_profile_interests.js index 6257e69ed..6a4a65e2c 100644 --- a/web/app/assets/javascripts/accounts_profile_interests.js +++ b/web/app/assets/javascripts/accounts_profile_interests.js @@ -62,9 +62,9 @@ var $cowritingGenreList = $cowritingGenres.find(GENRE_LIST_SELECTOR) var $cowritingPurpose = $screen.find('#cowriting-purpose') - var $btnCancel = $screen.find('#account-edit-profile-cancel') - var $btnBack = $screen.find('#account-edit-profile-back') - var $btnSubmit = $screen.find('#account-edit-profile-submit') + var $btnCancel = $screen.find('.account-edit-profile-cancel') + var $btnBack = $screen.find('.account-edit-profile-back') + var $btnSubmit = $screen.find('.account-edit-profile-submit') function beforeShow(data) { } diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 67fc330be..6cce9de0e 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -37,22 +37,22 @@ var $youTubeSampleList = $screen.find(".sample-list[source-type='youtube']") // buttons - var $btnAddJkRecording = $screen.find('.btn-add-jk-recording'); - var $btnCancel = $screen.find('.account-edit-profile-cancel'); - var $btnBack = $screen.find('.account-edit-profile-back'); - var $btnSubmit = $screen.find('.account-edit-profile-submit'); + var $btnAddJkRecording = $screen.find('.btn-add-jk-recording') + var $btnCancel = parent.find('.account-edit-profile-cancel') + var $btnBack = parent.find('.account-edit-profile-back') + var $btnSubmit = parent.find('.account-edit-profile-submit') - var urlValidator=null; - var soundCloudValidator=null; - var reverbNationValidator=null; - var bandCampValidator=null; - var fandalismValidator=null; - var youTubeValidator=null; - var facebookValidator=null; - var twitterValidator=null; - var soundCloudRecordingValidator=null; - var youTubeRecordingValidator=null; + var urlValidator=null + var soundCloudValidator=null + var reverbNationValidator=null + var bandCampValidator=null + var fandalismValidator=null + var youTubeValidator=null + var facebookValidator=null + var twitterValidator=null + var soundCloudRecordingValidator=null + var youTubeRecordingValidator=null function beforeShow(data) { } @@ -221,12 +221,12 @@ }) } - function enableSubmits() { - $btnSubmit.on("click", function(evt) { - evt.stopPropagation(); + function enableSubmits() { + $btnSubmit.off("click").on("click", function(e) { + e.stopPropagation(); handleUpdateProfile(); return false; - }); + }) $btnSubmit.removeClass("disabled") } diff --git a/web/app/views/clients/_account.html.erb b/web/app/views/clients/_account.html.erb index 29d806a94..02afa69d4 100644 --- a/web/app/views/clients/_account.html.erb +++ b/web/app/views/clients/_account.html.erb @@ -112,12 +112,12 @@