diff --git a/admin/Gemfile b/admin/Gemfile index 281cde012..567f0e491 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -1,7 +1,7 @@ source 'http://rubygems.org' source 'https://jamjam:blueberryjam@int.jamkazam.com/gems/' -devenv = ENV["BUILD_NUMBER"].nil? || ENV["TEST_WWW"] == "1" +devenv = ENV["BUILD_NUMBER"].nil? if devenv gem 'jam_db', :path=> "../db/target/ruby_package" @@ -72,9 +72,6 @@ gem 'postgres_ext', '1.0.0' gem 'resque_mailer' gem 'rest-client' -gem 'geokit-rails' -gem 'postgres_ext', '1.0.0' - group :libv8 do gem 'libv8', "~> 3.11.8" end @@ -104,11 +101,7 @@ group :development, :test do gem 'capybara' gem 'rspec-rails' gem 'guard-rspec', '0.5.5' - gem 'jasmine', '1.3.1' - gem 'pry' - gem 'pry-remote' - gem 'pry-stack_explorer' - gem 'pry-debugger' + gem 'jasmine', '1.3.1' gem 'execjs', '1.4.0' gem 'therubyracer' #, '0.11.0beta8' gem 'factory_girl_rails', '4.1.0' @@ -120,4 +113,12 @@ end group :test do gem 'simplecov', '~> 0.7.1' gem 'simplecov-rcov' -end \ No newline at end of file + gem 'capybara-webkit' + gem 'capybara-screenshot' + gem 'poltergeist' +end + +gem 'pry' +gem 'pry-remote' +gem 'pry-stack_explorer' +gem 'pry-debugger' diff --git a/admin/app/admin/dashboard.rb b/admin/app/admin/dashboard.rb index fc5ae8e92..a22643621 100644 --- a/admin/app/admin/dashboard.rb +++ b/admin/app/admin/dashboard.rb @@ -38,7 +38,7 @@ ActiveAdmin.register_page "Dashboard" do end end end - end + end end # column do diff --git a/admin/app/admin/email_batch.rb b/admin/app/admin/email_batch.rb index e5743eeb8..e31ec5cce 100644 --- a/admin/app/admin/email_batch.rb +++ b/admin/app/admin/email_batch.rb @@ -115,7 +115,7 @@ ActiveAdmin.register JamRuby::EmailBatch, :as => 'Batch Emails' do end member_action :batch_send, :method => :get do - resource.deliver_batch + resource.deliver_batch_async redirect_to admin_batch_email_path(resource.id) end diff --git a/admin/app/admin/email_error_batch.rb b/admin/app/admin/email_error_batch.rb deleted file mode 100644 index 9c4ce9280..000000000 --- a/admin/app/admin/email_error_batch.rb +++ /dev/null @@ -1,29 +0,0 @@ -ActiveAdmin.register JamRuby::EmailError, :as => 'Email Errors' do - - menu :label => 'Email Errors', :parent => 'Email' - - config.batch_actions = false - config.filters = false - config.clear_action_items! - - index do - column 'User' do |eerr| - eerr.user ? link_to(eerr.user.name, admin_user_path(eerr.user_id)) : 'N/A' - end - column 'Error Type' do |eerr| eerr.error_type end - column 'Email Address' do |eerr| eerr.email_address end - column 'Status' do |eerr| eerr.status end - column 'Reason' do |eerr| eerr.reason end - column 'Email Date' do |eerr| eerr.email_date end - end - - controller do - - def scoped_collection - @eerrors ||= end_of_association_chain - .includes([:user]) - .order('email_date DESC') - end - end - -end diff --git a/admin/app/admin/feeds.rb b/admin/app/admin/feeds.rb new file mode 100644 index 000000000..da8fde49a --- /dev/null +++ b/admin/app/admin/feeds.rb @@ -0,0 +1,221 @@ +ActiveAdmin.register_page 'Feed' do + content do + + # get user information via params + user_id = nil + user_id = params[:feed][:user_id] if params[:feed] && params[:feed][:user_id] != '' + user_name = 'All' + user_name = User.find(user_id).to_label if user_id + + render :partial => 'form', locals: {user_name: user_name, user_id: user_id } + + page = (params[:page] ||= 1).to_i + per_page = 10 + offset = (page - 1) * per_page + + # get feed ids + where_sql = '' + where_sql = "where user_id = '#{user_id}'" if user_id + sql_feed_ids = "SELECT id, 'music_sessions' as type, created_at FROM music_sessions #{where_sql} + UNION ALL + SELECT DISTINCT recording_id as id, 'recordings' as type, created_at FROM recorded_tracks #{where_sql} + UNION ALL + SELECT id, 'diagnostics' as type, created_at FROM diagnostics #{where_sql} + ORDER BY created_at DESC + OFFSET #{offset} + LIMIT #{per_page};" + + sql_feed_count = "SELECT COUNT(*) FROM ( + SELECT id, 'music_sessions' as type, created_at FROM music_sessions #{where_sql} + UNION ALL + SELECT DISTINCT recording_id as id, 'recordings' as type, created_at FROM recorded_tracks #{where_sql} + UNION ALL + SELECT id, 'diagnostics' as type, created_at FROM diagnostics #{where_sql} + ORDER BY created_at DESC + ) AS IDS;" + feed_count = ActiveRecord::Base.connection.execute(sql_feed_count).values[0][0].to_i + id_types = ActiveRecord::Base.connection.execute(sql_feed_ids).values + + @feed_pages = WillPaginate::Collection.create(page, per_page) do |pager| + pager.total_entries = feed_count + pager.replace(id_types) + end + + div class: 'feed-pagination' do + will_paginate @feed_pages + end + + recordings = [] + sessions = [] + diagnostics = [] + id_types.each do |id_and_type| + if id_and_type[1] == "music_sessions" + sessions << JamRuby::MusicSession.find(id_and_type[0]) + elsif id_and_type[1] == "recordings" + recordings << JamRuby::Recording.find(id_and_type[0]) + elsif id_and_type[1] == "diagnostics" + diagnostics << JamRuby::Diagnostic.find(id_and_type[0]) + else + raise "Unknown type returned from feed ids" + end + end + + columns do + column do + panel "Music Sessions - #{user_name}" do + if sessions.count > 0 + table_for(sessions) do + column :creator do |msh| + link_to msh.creator.to_label, admin_feed_path({feed: {user_id: msh.creator.id}}) + end + column :created_at do |msh| msh.created_at.strftime('%b %d %Y, %H:%M') end + column :duration do |msh| "#{msh.duration_minutes.round(2)} minutes" end + column :members do |msh| + uu = msh.unique_users + if uu.length > 0 + uu.each do |u| + span link_to u.to_label + ', ', admin_feed_path({feed: {user_id: u.id}}) + end + else + span para 'No members' + end + end + + column :band do |msh| auto_link(msh.band, msh.band.try(:name)) end + column :fan_access do |msh| msh.fan_access end + column :plays do |msh| msh.plays.count end + column :likes do |msh| msh.likes.count end + column :comments do |msh| + if msh.comment_count > 0 + text_node "(#{msh.comment_count}) " + msh.comments.each do |comment| + text_node comment.user.to_label + ', ' + end + else + span para 'No comments' + end + end + end + else + span class: 'text-center' do + para 'No session activities.' + end + end + end + + panel "Recordings - #{user_name}" do + if recordings.count > 0 + table_for(recordings) do + column :starter do |rec| + link_to rec.owner.to_label, admin_feed_path({feed: {user_id: rec.owner.id}}) + end + column :mixes do |rec| + ul do + mixes = rec.mixes + if mixes.count > 0 + mixes.each do |mix| + li do + text_node "Created At: #{mix.created_at.strftime('%b %d %Y, %H:%M')}, " + text_node "Started At: #{mix.started_at.strftime('%b %d %Y, %H:%M')}, " + text_node "Completed At: #{mix.completed_at.strftime('%b %d %Y, %H:%M')}, " + text_node "Error Count: #{mix.error_count}, " + text_node "Error Reason: #{mix.error_reason}, " + text_node "Error Detail: #{mix.error_detail}, " + text_node "Download Count: #{mix.download_count}, " + if !mix.nil? && !mix[:ogg_url].nil? + span link_to 'Download OGG', mix.sign_url(3600, 'ogg') + else + text_node 'OGG download not available' + end + if !mix.nil? && !mix[:mp3_url].nil? + span link_to 'Download MP3', mix.sign_url(3600, 'mp3') + else + text_node 'MP3 download not available' + end + end + end + else + span para 'No mixes' + end + end + end + column :recorded_tracks do |rec| + ul do + rts = rec.recorded_tracks + if rts.count > 0 + rts.each do |gt| + li do + span link_to gt.musician.to_label, admin_feed_path({feed: {user_id: gt.musician.id}}) + span ", #{gt.instrument_id}, " + span "Download Count: #{gt.download_count}, " + span "Fully uploaded: #{gt.fully_uploaded}, " + span "Upload failures: #{gt.upload_failures}, " + span "Part failures: #{gt.part_failures}, " + if gt[:url] + # span link_to 'Download', gt.sign_url(3600) + else + span 'No track available' + end + end + end + else + span para 'No recorded tracks' + end + end + end + column :claimed_recordings do |rec| + ul do + crs = rec.claimed_recordings + if crs.count > 0 + crs.each do |cr| + li do + span cr.name + span link_to cr.user.to_label, admin_feed_path({feed: {user_id: cr.user.id}}) + span ", Public: #{cr.is_public}" + end + end + else + span para 'No claimed recordings' + end + end + end + end + else + span class: 'text-center' do + para 'No recording activities.' + end + end + end + + panel "Diagnostics - #{user_name}" do + if diagnostics.count > 0 then + table_for(diagnostics) do + column :user do |d| + span link_to d.user.to_label, admin_feed_path({feed: {user_id: d.user.id}}) + end + column :created_at do |d| d.created_at.strftime('%b %d %Y, %H:%M') end + column :type + column :creator + column :data do |d| + span style: "white-space: pre;" do + begin + JSON.pretty_generate(JSON.parse(d.data)) + rescue + d.data + end + end + end + end + else + span class: 'text-center' do + para 'No diagnostic activities.' + end + end + end + end + end + div class: 'feed-pagination' do + will_paginate @feed_pages + end + end +end \ No newline at end of file diff --git a/admin/app/admin/jam_ruby_users.rb b/admin/app/admin/jam_ruby_users.rb index fa2ae8051..79f138df0 100644 --- a/admin/app/admin/jam_ruby_users.rb +++ b/admin/app/admin/jam_ruby_users.rb @@ -103,7 +103,7 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do autocomplete :user, :email, :full => true, :display_value => :autocomplete_display_name def get_autocomplete_items(parameters) - items = User.select("DISTINCT email, first_name, last_name, id").where(["email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?", "%#{parameters[:term]}%", "%#{parameters[:term]}%", "%#{parameters[:term]}%"]) + User.select("email, first_name, last_name, id").where(["email ILIKE ? OR first_name ILIKE ? OR last_name ILIKE ?", "%#{parameters[:term]}%", "%#{parameters[:term]}%", "%#{parameters[:term]}%"]) end def create diff --git a/admin/app/admin/latency_tester.rb b/admin/app/admin/latency_tester.rb new file mode 100644 index 000000000..a62e506d4 --- /dev/null +++ b/admin/app/admin/latency_tester.rb @@ -0,0 +1,32 @@ +ActiveAdmin.register JamRuby::LatencyTester, :as => 'LatencyTester' do + + config.filters = true + config.per_page = 50 + config.clear_action_items! + config.sort_order = "client_id" + menu :parent => 'Operations' + + controller do + def scoped_collection + @latency_testers ||= end_of_association_chain + .order('client_id') + end + end + + + index :as => :block do |latency_tester| + div :for => latency_tester do + h3 "#{latency_tester.client_id}" + columns do + column do + panel 'Details' do + attributes_table_for(latency_tester) do + row :connection do |latency_tester| latency_tester.connection ? "last updated at: #{latency_tester.connection.updated_at}" : "no connection" end + end + end + end + end + end + end +end + diff --git a/admin/app/assets/stylesheets/active_admin.css.scss b/admin/app/assets/stylesheets/active_admin.css.scss index 4798f7467..48aa7bb98 100644 --- a/admin/app/assets/stylesheets/active_admin.css.scss +++ b/admin/app/assets/stylesheets/active_admin.css.scss @@ -9,6 +9,7 @@ /* *= require jquery.ui.all +*= require custom */ // Active Admin's got SASS! @import "active_admin/mixins"; diff --git a/admin/app/assets/stylesheets/custom.css.scss b/admin/app/assets/stylesheets/custom.css.scss index 97651a7af..f253c6e8d 100644 --- a/admin/app/assets/stylesheets/custom.css.scss +++ b/admin/app/assets/stylesheets/custom.css.scss @@ -2,4 +2,24 @@ .version-info { font-size:small; color:lightgray; +} + +.text-center { + text-align: center; +} + +.feed-pagination { + height: 20px; + margin-bottom: 15px; + .pagination { + float: left !important; + + ul { + list-style-type: none; + + li { + float: left; + } + } + } } \ No newline at end of file diff --git a/admin/app/views/admin/feed/_form.html.erb b/admin/app/views/admin/feed/_form.html.erb new file mode 100644 index 000000000..87ff1936b --- /dev/null +++ b/admin/app/views/admin/feed/_form.html.erb @@ -0,0 +1,6 @@ +<%= semantic_form_for :feed, url: admin_feed_path, method: :get do |f| %> + <%= f.inputs do %> + <%= f.input :user, :as => :autocomplete, :url => autocomplete_user_email_admin_users_path, :input_html => { :id_element => "#feed_user_id" } %> + <%= f.input :user_id, :as => :hidden %> + <% end %> +<% end %> \ No newline at end of file diff --git a/admin/log/phantomjs.out b/admin/log/phantomjs.out new file mode 100644 index 000000000..e69de29bb diff --git a/admin/spec/factories.rb b/admin/spec/factories.rb index a0e17bf6b..a2d058d80 100644 --- a/admin/spec/factories.rb +++ b/admin/spec/factories.rb @@ -28,6 +28,17 @@ FactoryGirl.define do end end end + + factory :connection, :class => JamRuby::Connection do + sequence(:client_id) { |n| "Client#{n}" } + ip_address "1.1.1.1" + as_musician true + addr 0 + locidispid 0 + client_type 'client' + association :user, factory: :user + end + factory :artifact_update, :class => JamRuby::ArtifactUpdate do sequence(:version) { |n| "0.1.#{n}" } uri { "http://somewhere/jkclient.msi" } @@ -46,4 +57,145 @@ FactoryGirl.define do description { |n| "Instrument #{n}" } end + factory :genre, :class => JamRuby::Genre do + description { |n| "Genre #{n}" } + end + + factory :music_session, :class => JamRuby::MusicSession do + sequence(:name) { |n| "Music Session #{n}" } + sequence(:description) { |n| "Music Session Description #{n}" } + fan_chat true + fan_access true + approval_required false + musician_access true + legal_terms true + language 'english' + legal_policy 'standard' + genre JamRuby::Genre.first + association :creator, :factory => :user + end + + factory :music_session_user_history, :class => JamRuby::MusicSessionUserHistory do + ignore do + history nil + user nil + end + + music_session_id { history.id } + user_id { user.id } + sequence(:client_id) { |n| "Connection #{n}" } + end + + factory :recorded_track, :class => JamRuby::RecordedTrack do + instrument JamRuby::Instrument.first + sound 'stereo' + sequence(:client_id) { |n| "client_id-#{n}"} + sequence(:track_id) { |n| "track_id-#{n}"} + sequence(:client_track_id) { |n| "client_track_id-#{n}"} + md5 'abc' + length 1 + fully_uploaded true + association :user, factory: :user + association :recording, factory: :recording + end + + factory :recording, :class => JamRuby::Recording do + + association :owner, factory: :user + association :music_session, factory: :active_music_session + + factory :recording_with_track do + before(:create) { |recording, evaluator| + recording.recorded_tracks << FactoryGirl.create(:recorded_track, recording: recording, user: evaluator.owner) + } + end + end + + factory :claimed_recording, :class => JamRuby::ClaimedRecording do + sequence(:name) { |n| "name-#{n}" } + sequence(:description) { |n| "description-#{n}" } + is_public true + association :genre, factory: :genre + association :user, factory: :user + + before(:create) { |claimed_recording| + claimed_recording.recording = FactoryGirl.create(:recording_with_track, owner: claimed_recording.user) + } + + end + + factory :mix, :class => JamRuby::Mix do + started_at Time.now + completed_at Time.now + ogg_md5 'abc' + ogg_length 1 + sequence(:ogg_url) { |n| "recordings/ogg/#{n}" } + mp3_md5 'abc' + mp3_length 1 + sequence(:mp3_url) { |n| "recordings/mp3/#{n}" } + completed true + + before(:create) {|mix| + user = FactoryGirl.create(:user) + mix.recording = FactoryGirl.create(:recording_with_track, owner: user) + mix.recording.claimed_recordings << FactoryGirl.create(:claimed_recording, user: user, recording: mix.recording) + } + end + + factory :diagnostic, :class => JamRuby::Diagnostic do + type JamRuby::Diagnostic::NO_HEARTBEAT_ACK + creator JamRuby::Diagnostic::CLIENT + data Faker::Lorem.sentence + association :user, factory: :user + end + + factory :active_music_session_no_user_history, :class => JamRuby::ActiveMusicSession do + + association :creator, factory: :user + + ignore do + name "My Music Session" + description "Come Music Session" + fan_chat true + fan_access true + approval_required false + musician_access true + legal_terms true + genre JamRuby::Genre.first + band nil + end + + + before(:create) do |session, evaluator| + music_session = FactoryGirl.create(:music_session, name: evaluator.name, description: evaluator.description, fan_chat: evaluator.fan_chat, + fan_access: evaluator.fan_access, approval_required: evaluator.approval_required, musician_access: evaluator.musician_access, + genre: evaluator.genre, creator: evaluator.creator, band: evaluator.band) + session.id = music_session.id + end + + factory :active_music_session do + after(:create) { |session| + FactoryGirl.create(:music_session_user_history, :history => session.music_session, :user => session.creator) + } + + factory :music_session_with_mount do + association :mount, :factory => :icecast_mount + end + end + end + + factory :latency_tester, :class => JamRuby::LatencyTester do + ignore do + connection nil + make_connection true + end + + sequence(:client_id) { |n| "LatencyTesterClientId-#{n}" } + + after(:create) do |latency_tester, evaluator| + latency_tester.connection = evaluator.connection if evaluator.connection + latency_tester.connection = FactoryGirl.create(:connection, client_type: Connection::TYPE_LATENCY_TESTER, client_id: latency_tester.client_id) if evaluator.make_connection + latency_tester.save + end + end end diff --git a/admin/spec/features/feeds_spec.rb b/admin/spec/features/feeds_spec.rb new file mode 100644 index 000000000..43505703a --- /dev/null +++ b/admin/spec/features/feeds_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe 'Feeds' do + + subject { page } + + before(:each) do + MusicSession.delete_all + Recording.delete_all + Diagnostic.delete_all + User.delete_all + end + + let(:admin) { FactoryGirl.create(:admin) } + let(:user) { FactoryGirl.create(:user) } + + let(:music_session) { FactoryGirl.create(:music_session, :creator => user) } + let(:recording) { FactoryGirl.create(:recording_with_track, :owner => user) } + let(:diagnostic) { FactoryGirl.create(:diagnostic, :user => user) } + + context 'empty dashboard' do + before(:each) do + visit admin_feed_path + end + + it { should have_selector('h2', text: 'Feed') } + + it 'has no feeds' do + should have_selector('p', text: 'No session activities.') + should have_selector('p', text: 'No recording activities.') + should have_selector('p', text: 'No diagnostic activities.') + end + end + + + context 'admin enters a user name' do + before(:each) do + user.touch + visit admin_feed_path + end + + it 'auto-completes with email + full name', :js => true do + + within('form.feed') do + fill_in 'feed_user', with: user.email[0..3] + end + + page.execute_script %Q{ $('form.feed input#feed_user').trigger('focus') } + page.execute_script %Q{ $('form.feed input#feed_user').trigger('keydown') } + find('a.ui-corner-all', text: user.to_label).trigger(:click) + should have_selector('form.feed #feed_user', user.to_label) + should have_selector('form.feed #feed_user_id[value="' + user.id + '"]', visible:false) + end + end + + context 'with existing activities' do + before(:each) do + music_session.touch + recording.touch + diagnostic.touch + visit admin_feed_path + end + + it 'shows session, recording, diagnostic' do + should have_selector("tr#jam_ruby_music_session_#{music_session.id}") + should have_selector("tr#jam_ruby_recording_#{recording.id}") + should have_selector("tr#jam_ruby_diagnostic_#{diagnostic.id}") + end + + it 'shows activities for one user', :js => true do + within('form.feed') do + fill_in 'feed_user', with: user.email[0..3] + end + + page.execute_script %Q{ $('form.feed input#feed_user').trigger('focus') } + page.execute_script %Q{ $('form.feed input#feed_user').trigger('keydown') } + find('a.ui-corner-all', text: user.to_label).trigger(:click) + should have_selector('form.feed #feed_user', user.to_label) + should have_selector('form.feed #feed_user_id[value="' + user.id + '"]', visible:false) + + page.execute_script %Q{ $('form.feed').trigger('submit') } + + should have_selector("tr#jam_ruby_music_session_#{music_session.id}") + should have_selector("tr#jam_ruby_recording_#{recording.id}") + should have_selector("tr#jam_ruby_diagnostic_#{diagnostic.id}") + end + end +end \ No newline at end of file diff --git a/admin/spec/features/latency_testers_spec.rb b/admin/spec/features/latency_testers_spec.rb new file mode 100644 index 000000000..16e72fa9a --- /dev/null +++ b/admin/spec/features/latency_testers_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe 'Feeds' do + + subject { page } + + before(:each) do + end + + + describe "latency_tester with connection" do + let!(:latency_tester) {FactoryGirl.create(:latency_tester)} + + before(:each) do + visit admin_latency_testers_path + end + + it "shows connection info" do + should have_selector('td', text: "last updated at: #{latency_tester.connection.updated_at}") + end + end + + describe "latency_tester with no connection" do + let!(:latency_tester) {FactoryGirl.create(:latency_tester, client_id: 'abc', make_connection: false)} + + before(:each) do + visit admin_latency_testers_path + end + + it "shows no connection" do + should have_selector('td', text: "no connection") + end + end + + +end diff --git a/admin/spec/spec_helper.rb b/admin/spec/spec_helper.rb index f7ddad41a..d3e24c62a 100644 --- a/admin/spec/spec_helper.rb +++ b/admin/spec/spec_helper.rb @@ -22,6 +22,9 @@ require 'rspec/autorun' # load capybara require 'capybara/rails' +require 'capybara/rspec' +require 'capybara-screenshot/rspec' +require 'capybara/poltergeist' #include Rails.application.routes.url_helpers @@ -30,6 +33,11 @@ require 'capybara/rails' Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} +Capybara.register_driver :poltergeist do |app| + driver = Capybara::Poltergeist::Driver.new(app, { debug: false, phantomjs_logger: File.open('log/phantomjs.out', 'w') }) +end +Capybara.javascript_driver = :poltergeist +Capybara.default_wait_time = 10 RSpec.configure do |config| # ## Mock Framework @@ -46,7 +54,7 @@ RSpec.configure do |config| # If you're not using ActiveRecord, or you'd prefer not to run each of your # examples within a transaction, remove the following line or assign false # instead of true. - config.use_transactional_fixtures = true + config.use_transactional_fixtures = false # If true, the base class of anonymous controllers will be inferred # automatically. This will be the default behavior in future versions of diff --git a/admin/spec/support/snapshot.rb b/admin/spec/support/snapshot.rb new file mode 100644 index 000000000..e77123edf --- /dev/null +++ b/admin/spec/support/snapshot.rb @@ -0,0 +1,30 @@ +module Snapshot + + SS_PATH = 'snapshots.html' + + def set_up_snapshot(filepath = SS_PATH) + @size = [1280, 720] #arbitrary + @file = File.new(filepath, "w+") + @file.puts "" + @file.puts "

Snapshot #{ENV["BUILD_NUMBER"]} - #{@size[0]}x#{@size[1]}

" + end + + def snapshot_example + page.driver.resize(@size[0], @size[1]) + @file.puts "

Example name: #{get_description}



" + end + + def snap!(title = get_description) + base64 = page.driver.render_base64(:png) + @file.puts '

' + title + '

' + @file.puts '' + title +'' + @file.puts '


' + end + + def tear_down_snapshot + @file.puts "" + @file.close() + end + +end + diff --git a/atlassian-ide-plugin.xml b/atlassian-ide-plugin.xml new file mode 100644 index 000000000..858eed55c --- /dev/null +++ b/atlassian-ide-plugin.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/db/geodata/README.txt b/db/geodata/README.txt index ff42edb3b..4988404c0 100644 --- a/db/geodata/README.txt +++ b/db/geodata/README.txt @@ -1 +1,10 @@ this is just for getting this maxmind data over there so i can use it. + +source for iso3166-1 data: + +http://dev.maxmind.com/static/csv/codes/iso3166.csv + +source for iso3166-2 data (compatible): + +http://geolite.maxmind.com/download/geoip/misc/region_codes.csv + diff --git a/db/geodata/ca_region.csv b/db/geodata/ca_region.csv deleted file mode 100644 index f24bd3902..000000000 --- a/db/geodata/ca_region.csv +++ /dev/null @@ -1,13 +0,0 @@ -AB,Alberta -BC,British Columbia -MB,Manitoba -NB,New Brunswick -NL,Newfoundland and Labrador -NS,Nova Scotia -NT,Northwest Territories -NU,Nunavut -ON,Ontario -PE,Prince Edward Island -QC,Quebec -SK,Saskatchewan -YT,Yukon diff --git a/db/geodata/region_codes.csv b/db/geodata/region_codes.csv new file mode 100644 index 000000000..bcb3ec768 --- /dev/null +++ b/db/geodata/region_codes.csv @@ -0,0 +1,4106 @@ +AD,02,"Canillo" +AD,03,"Encamp" +AD,04,"La Massana" +AD,05,"Ordino" +AD,06,"Sant Julia de Loria" +AD,07,"Andorra la Vella" +AD,08,"Escaldes-Engordany" +AE,01,"Abu Dhabi" +AE,02,"Ajman" +AE,03,"Dubai" +AE,04,"Fujairah" +AE,05,"Ras Al Khaimah" +AE,06,"Sharjah" +AE,07,"Umm Al Quwain" +AF,01,"Badakhshan" +AF,02,"Badghis" +AF,03,"Baghlan" +AF,05,"Bamian" +AF,06,"Farah" +AF,07,"Faryab" +AF,08,"Ghazni" +AF,09,"Ghowr" +AF,10,"Helmand" +AF,11,"Herat" +AF,13,"Kabol" +AF,14,"Kapisa" +AF,17,"Lowgar" +AF,18,"Nangarhar" +AF,19,"Nimruz" +AF,23,"Kandahar" +AF,24,"Kondoz" +AF,26,"Takhar" +AF,27,"Vardak" +AF,28,"Zabol" +AF,29,"Paktika" +AF,30,"Balkh" +AF,31,"Jowzjan" +AF,32,"Samangan" +AF,33,"Sar-e Pol" +AF,34,"Konar" +AF,35,"Laghman" +AF,36,"Paktia" +AF,37,"Khowst" +AF,38,"Nurestan" +AF,39,"Oruzgan" +AF,40,"Parvan" +AF,41,"Daykondi" +AF,42,"Panjshir" +AG,01,"Barbuda" +AG,03,"Saint George" +AG,04,"Saint John" +AG,05,"Saint Mary" +AG,06,"Saint Paul" +AG,07,"Saint Peter" +AG,08,"Saint Philip" +AG,09,"Redonda" +AL,40,"Berat" +AL,41,"Diber" +AL,42,"Durres" +AL,43,"Elbasan" +AL,44,"Fier" +AL,45,"Gjirokaster" +AL,46,"Korce" +AL,47,"Kukes" +AL,48,"Lezhe" +AL,49,"Shkoder" +AL,50,"Tirane" +AL,51,"Vlore" +AM,01,"Aragatsotn" +AM,02,"Ararat" +AM,03,"Armavir" +AM,04,"Geghark'unik'" +AM,05,"Kotayk'" +AM,06,"Lorri" +AM,07,"Shirak" +AM,08,"Syunik'" +AM,09,"Tavush" +AM,10,"Vayots' Dzor" +AM,11,"Yerevan" +AO,01,"Benguela" +AO,02,"Bie" +AO,03,"Cabinda" +AO,04,"Cuando Cubango" +AO,05,"Cuanza Norte" +AO,06,"Cuanza Sul" +AO,07,"Cunene" +AO,08,"Huambo" +AO,09,"Huila" +AO,12,"Malanje" +AO,13,"Namibe" +AO,14,"Moxico" +AO,15,"Uige" +AO,16,"Zaire" +AO,17,"Lunda Norte" +AO,18,"Lunda Sul" +AO,19,"Bengo" +AO,20,"Luanda" +AR,01,"Buenos Aires" +AR,02,"Catamarca" +AR,03,"Chaco" +AR,04,"Chubut" +AR,05,"Cordoba" +AR,06,"Corrientes" +AR,07,"Distrito Federal" +AR,08,"Entre Rios" +AR,09,"Formosa" +AR,10,"Jujuy" +AR,11,"La Pampa" +AR,12,"La Rioja" +AR,13,"Mendoza" +AR,14,"Misiones" +AR,15,"Neuquen" +AR,16,"Rio Negro" +AR,17,"Salta" +AR,18,"San Juan" +AR,19,"San Luis" +AR,20,"Santa Cruz" +AR,21,"Santa Fe" +AR,22,"Santiago del Estero" +AR,23,"Tierra del Fuego" +AR,24,"Tucuman" +AT,01,"Burgenland" +AT,02,"Karnten" +AT,03,"Niederosterreich" +AT,04,"Oberosterreich" +AT,05,"Salzburg" +AT,06,"Steiermark" +AT,07,"Tirol" +AT,08,"Vorarlberg" +AT,09,"Wien" +AU,01,"Australian Capital Territory" +AU,02,"New South Wales" +AU,03,"Northern Territory" +AU,04,"Queensland" +AU,05,"South Australia" +AU,06,"Tasmania" +AU,07,"Victoria" +AU,08,"Western Australia" +AZ,01,"Abseron" +AZ,02,"Agcabadi" +AZ,03,"Agdam" +AZ,04,"Agdas" +AZ,05,"Agstafa" +AZ,06,"Agsu" +AZ,07,"Ali Bayramli" +AZ,08,"Astara" +AZ,09,"Baki" +AZ,10,"Balakan" +AZ,11,"Barda" +AZ,12,"Beylaqan" +AZ,13,"Bilasuvar" +AZ,14,"Cabrayil" +AZ,15,"Calilabad" +AZ,16,"Daskasan" +AZ,17,"Davaci" +AZ,18,"Fuzuli" +AZ,19,"Gadabay" +AZ,20,"Ganca" +AZ,21,"Goranboy" +AZ,22,"Goycay" +AZ,23,"Haciqabul" +AZ,24,"Imisli" +AZ,25,"Ismayilli" +AZ,26,"Kalbacar" +AZ,27,"Kurdamir" +AZ,28,"Lacin" +AZ,29,"Lankaran" +AZ,30,"Lankaran" +AZ,31,"Lerik" +AZ,32,"Masalli" +AZ,33,"Mingacevir" +AZ,34,"Naftalan" +AZ,35,"Naxcivan" +AZ,36,"Neftcala" +AZ,37,"Oguz" +AZ,38,"Qabala" +AZ,39,"Qax" +AZ,40,"Qazax" +AZ,41,"Qobustan" +AZ,42,"Quba" +AZ,43,"Qubadli" +AZ,44,"Qusar" +AZ,45,"Saatli" +AZ,46,"Sabirabad" +AZ,47,"Saki" +AZ,48,"Saki" +AZ,49,"Salyan" +AZ,50,"Samaxi" +AZ,51,"Samkir" +AZ,52,"Samux" +AZ,53,"Siyazan" +AZ,54,"Sumqayit" +AZ,55,"Susa" +AZ,56,"Susa" +AZ,57,"Tartar" +AZ,58,"Tovuz" +AZ,59,"Ucar" +AZ,60,"Xacmaz" +AZ,61,"Xankandi" +AZ,62,"Xanlar" +AZ,63,"Xizi" +AZ,64,"Xocali" +AZ,65,"Xocavand" +AZ,66,"Yardimli" +AZ,67,"Yevlax" +AZ,68,"Yevlax" +AZ,69,"Zangilan" +AZ,70,"Zaqatala" +AZ,71,"Zardab" +BA,01,"Federation of Bosnia and Herzegovina" +BA,03,"Brcko District" +BA,02,"Republika Srpska" +BB,01,"Christ Church" +BB,02,"Saint Andrew" +BB,03,"Saint George" +BB,04,"Saint James" +BB,05,"Saint John" +BB,06,"Saint Joseph" +BB,07,"Saint Lucy" +BB,08,"Saint Michael" +BB,09,"Saint Peter" +BB,10,"Saint Philip" +BB,11,"Saint Thomas" +BD,81,"Dhaka" +BD,82,"Khulna" +BD,83,"Rajshahi" +BD,84,"Chittagong" +BD,85,"Barisal" +BD,86,"Sylhet" +BD,87,"Rangpur" +BE,01,"Antwerpen" +BE,03,"Hainaut" +BE,04,"Liege" +BE,05,"Limburg" +BE,06,"Luxembourg" +BE,07,"Namur" +BE,08,"Oost-Vlaanderen" +BE,09,"West-Vlaanderen" +BE,10,"Brabant Wallon" +BE,11,"Brussels Hoofdstedelijk Gewest" +BE,12,"Vlaams-Brabant" +BE,13,"Flanders" +BE,14,"Wallonia" +BF,15,"Bam" +BF,19,"Boulkiemde" +BF,20,"Ganzourgou" +BF,21,"Gnagna" +BF,28,"Kouritenga" +BF,33,"Oudalan" +BF,34,"Passore" +BF,36,"Sanguie" +BF,40,"Soum" +BF,42,"Tapoa" +BF,44,"Zoundweogo" +BF,45,"Bale" +BF,46,"Banwa" +BF,47,"Bazega" +BF,48,"Bougouriba" +BF,49,"Boulgou" +BF,50,"Gourma" +BF,51,"Houet" +BF,52,"Ioba" +BF,53,"Kadiogo" +BF,54,"Kenedougou" +BF,55,"Komoe" +BF,56,"Komondjari" +BF,57,"Kompienga" +BF,58,"Kossi" +BF,59,"Koulpelogo" +BF,60,"Kourweogo" +BF,61,"Leraba" +BF,62,"Loroum" +BF,63,"Mouhoun" +BF,64,"Namentenga" +BF,65,"Naouri" +BF,66,"Nayala" +BF,67,"Noumbiel" +BF,68,"Oubritenga" +BF,69,"Poni" +BF,70,"Sanmatenga" +BF,71,"Seno" +BF,72,"Sissili" +BF,73,"Sourou" +BF,74,"Tuy" +BF,75,"Yagha" +BF,76,"Yatenga" +BF,77,"Ziro" +BF,78,"Zondoma" +BG,33,"Mikhaylovgrad" +BG,38,"Blagoevgrad" +BG,39,"Burgas" +BG,40,"Dobrich" +BG,41,"Gabrovo" +BG,42,"Grad Sofiya" +BG,43,"Khaskovo" +BG,44,"Kurdzhali" +BG,45,"Kyustendil" +BG,46,"Lovech" +BG,47,"Montana" +BG,48,"Pazardzhik" +BG,49,"Pernik" +BG,50,"Pleven" +BG,51,"Plovdiv" +BG,52,"Razgrad" +BG,53,"Ruse" +BG,54,"Shumen" +BG,55,"Silistra" +BG,56,"Sliven" +BG,57,"Smolyan" +BG,58,"Sofiya" +BG,59,"Stara Zagora" +BG,60,"Turgovishte" +BG,61,"Varna" +BG,62,"Veliko Turnovo" +BG,63,"Vidin" +BG,64,"Vratsa" +BG,65,"Yambol" +BH,01,"Al Hadd" +BH,02,"Al Manamah" +BH,05,"Jidd Hafs" +BH,06,"Sitrah" +BH,08,"Al Mintaqah al Gharbiyah" +BH,09,"Mintaqat Juzur Hawar" +BH,10,"Al Mintaqah ash Shamaliyah" +BH,11,"Al Mintaqah al Wusta" +BH,12,"Madinat" +BH,13,"Ar Rifa" +BH,14,"Madinat Hamad" +BH,15,"Al Muharraq" +BH,16,"Al Asimah" +BH,17,"Al Janubiyah" +BH,18,"Ash Shamaliyah" +BH,19,"Al Wusta" +BI,02,"Bujumbura" +BI,09,"Bubanza" +BI,10,"Bururi" +BI,11,"Cankuzo" +BI,12,"Cibitoke" +BI,13,"Gitega" +BI,14,"Karuzi" +BI,15,"Kayanza" +BI,16,"Kirundo" +BI,17,"Makamba" +BI,18,"Muyinga" +BI,19,"Ngozi" +BI,20,"Rutana" +BI,21,"Ruyigi" +BI,22,"Muramvya" +BI,23,"Mwaro" +BJ,07,"Alibori" +BJ,08,"Atakora" +BJ,09,"Atlanyique" +BJ,10,"Borgou" +BJ,11,"Collines" +BJ,12,"Kouffo" +BJ,13,"Donga" +BJ,14,"Littoral" +BJ,15,"Mono" +BJ,16,"Oueme" +BJ,17,"Plateau" +BJ,18,"Zou" +BM,01,"Devonshire" +BM,02,"Hamilton" +BM,03,"Hamilton" +BM,04,"Paget" +BM,05,"Pembroke" +BM,06,"Saint George" +BM,07,"Saint George's" +BM,08,"Sandys" +BM,09,"Smiths" +BM,10,"Southampton" +BM,11,"Warwick" +BN,07,"Alibori" +BN,08,"Belait" +BN,09,"Brunei and Muara" +BN,10,"Temburong" +BN,11,"Collines" +BN,12,"Kouffo" +BN,13,"Donga" +BN,14,"Littoral" +BN,15,"Tutong" +BN,16,"Oueme" +BN,17,"Plateau" +BN,18,"Zou" +BO,01,"Chuquisaca" +BO,02,"Cochabamba" +BO,03,"El Beni" +BO,04,"La Paz" +BO,05,"Oruro" +BO,06,"Pando" +BO,07,"Potosi" +BO,08,"Santa Cruz" +BO,09,"Tarija" +BR,01,"Acre" +BR,02,"Alagoas" +BR,03,"Amapa" +BR,04,"Amazonas" +BR,05,"Bahia" +BR,06,"Ceara" +BR,07,"Distrito Federal" +BR,08,"Espirito Santo" +BR,11,"Mato Grosso do Sul" +BR,13,"Maranhao" +BR,14,"Mato Grosso" +BR,15,"Minas Gerais" +BR,16,"Para" +BR,17,"Paraiba" +BR,18,"Parana" +BR,20,"Piaui" +BR,21,"Rio de Janeiro" +BR,22,"Rio Grande do Norte" +BR,23,"Rio Grande do Sul" +BR,24,"Rondonia" +BR,25,"Roraima" +BR,26,"Santa Catarina" +BR,27,"Sao Paulo" +BR,28,"Sergipe" +BR,29,"Goias" +BR,30,"Pernambuco" +BR,31,"Tocantins" +BS,05,"Bimini" +BS,06,"Cat Island" +BS,10,"Exuma" +BS,13,"Inagua" +BS,15,"Long Island" +BS,16,"Mayaguana" +BS,18,"Ragged Island" +BS,22,"Harbour Island" +BS,23,"New Providence" +BS,24,"Acklins and Crooked Islands" +BS,25,"Freeport" +BS,26,"Fresh Creek" +BS,27,"Governor's Harbour" +BS,28,"Green Turtle Cay" +BS,29,"High Rock" +BS,30,"Kemps Bay" +BS,31,"Marsh Harbour" +BS,32,"Nichollstown and Berry Islands" +BS,33,"Rock Sound" +BS,34,"Sandy Point" +BS,35,"San Salvador and Rum Cay" +BT,05,"Bumthang" +BT,06,"Chhukha" +BT,07,"Chirang" +BT,08,"Daga" +BT,09,"Geylegphug" +BT,10,"Ha" +BT,11,"Lhuntshi" +BT,12,"Mongar" +BT,13,"Paro" +BT,14,"Pemagatsel" +BT,15,"Punakha" +BT,16,"Samchi" +BT,17,"Samdrup" +BT,18,"Shemgang" +BT,19,"Tashigang" +BT,20,"Thimphu" +BT,21,"Tongsa" +BT,22,"Wangdi Phodrang" +BW,01,"Central" +BW,03,"Ghanzi" +BW,04,"Kgalagadi" +BW,05,"Kgatleng" +BW,06,"Kweneng" +BW,08,"North-East" +BW,09,"South-East" +BW,10,"Southern" +BW,11,"North-West" +BY,01,"Brestskaya Voblasts'" +BY,02,"Homyel'skaya Voblasts'" +BY,03,"Hrodzyenskaya Voblasts'" +BY,04,"Minsk" +BY,05,"Minskaya Voblasts'" +BY,06,"Mahilyowskaya Voblasts'" +BY,07,"Vitsyebskaya Voblasts'" +BZ,01,"Belize" +BZ,02,"Cayo" +BZ,03,"Corozal" +BZ,04,"Orange Walk" +BZ,05,"Stann Creek" +BZ,06,"Toledo" +CA,AB,"Alberta" +CA,BC,"British Columbia" +CA,MB,"Manitoba" +CA,NB,"New Brunswick" +CA,NL,"Newfoundland" +CA,NS,"Nova Scotia" +CA,NT,"Northwest Territories" +CA,NU,"Nunavut" +CA,ON,"Ontario" +CA,PE,"Prince Edward Island" +CA,QC,"Quebec" +CA,SK,"Saskatchewan" +CA,YT,"Yukon Territory" +CD,01,"Bandundu" +CD,02,"Equateur" +CD,04,"Kasai-Oriental" +CD,05,"Katanga" +CD,06,"Kinshasa" +CD,08,"Bas-Congo" +CD,09,"Orientale" +CD,10,"Maniema" +CD,11,"Nord-Kivu" +CD,12,"Sud-Kivu" +CF,01,"Bamingui-Bangoran" +CF,02,"Basse-Kotto" +CF,03,"Haute-Kotto" +CF,04,"Mambere-Kadei" +CF,05,"Haut-Mbomou" +CF,06,"Kemo" +CF,07,"Lobaye" +CF,08,"Mbomou" +CF,09,"Nana-Mambere" +CF,11,"Ouaka" +CF,12,"Ouham" +CF,13,"Ouham-Pende" +CF,14,"Cuvette-Ouest" +CF,15,"Nana-Grebizi" +CF,16,"Sangha-Mbaere" +CF,17,"Ombella-Mpoko" +CF,18,"Bangui" +CG,01,"Bouenza" +CG,04,"Kouilou" +CG,05,"Lekoumou" +CG,06,"Likouala" +CG,07,"Niari" +CG,08,"Plateaux" +CG,10,"Sangha" +CG,11,"Pool" +CG,12,"Brazzaville" +CG,13,"Cuvette" +CG,14,"Cuvette-Ouest" +CH,01,"Aargau" +CH,02,"Ausser-Rhoden" +CH,03,"Basel-Landschaft" +CH,04,"Basel-Stadt" +CH,05,"Bern" +CH,06,"Fribourg" +CH,07,"Geneve" +CH,08,"Glarus" +CH,09,"Graubunden" +CH,10,"Inner-Rhoden" +CH,11,"Luzern" +CH,12,"Neuchatel" +CH,13,"Nidwalden" +CH,14,"Obwalden" +CH,15,"Sankt Gallen" +CH,16,"Schaffhausen" +CH,17,"Schwyz" +CH,18,"Solothurn" +CH,19,"Thurgau" +CH,20,"Ticino" +CH,21,"Uri" +CH,22,"Valais" +CH,23,"Vaud" +CH,24,"Zug" +CH,25,"Zurich" +CH,26,"Jura" +CI,74,"Agneby" +CI,75,"Bafing" +CI,76,"Bas-Sassandra" +CI,77,"Denguele" +CI,78,"Dix-Huit Montagnes" +CI,79,"Fromager" +CI,80,"Haut-Sassandra" +CI,81,"Lacs" +CI,82,"Lagunes" +CI,83,"Marahoue" +CI,84,"Moyen-Cavally" +CI,85,"Moyen-Comoe" +CI,86,"N'zi-Comoe" +CI,87,"Savanes" +CI,88,"Sud-Bandama" +CI,89,"Sud-Comoe" +CI,90,"Vallee du Bandama" +CI,91,"Worodougou" +CI,92,"Zanzan" +CL,01,"Valparaiso" +CL,02,"Aisen del General Carlos Ibanez del Campo" +CL,03,"Antofagasta" +CL,04,"Araucania" +CL,05,"Atacama" +CL,06,"Bio-Bio" +CL,07,"Coquimbo" +CL,08,"Libertador General Bernardo O'Higgins" +CL,09,"Los Lagos" +CL,10,"Magallanes y de la Antartica Chilena" +CL,11,"Maule" +CL,12,"Region Metropolitana" +CL,13,"Tarapaca" +CL,14,"Los Lagos" +CL,15,"Tarapaca" +CL,16,"Arica y Parinacota" +CL,17,"Los Rios" +CM,04,"Est" +CM,05,"Littoral" +CM,07,"Nord-Ouest" +CM,08,"Ouest" +CM,09,"Sud-Ouest" +CM,10,"Adamaoua" +CM,11,"Centre" +CM,12,"Extreme-Nord" +CM,13,"Nord" +CM,14,"Sud" +CN,01,"Anhui" +CN,02,"Zhejiang" +CN,03,"Jiangxi" +CN,04,"Jiangsu" +CN,05,"Jilin" +CN,06,"Qinghai" +CN,07,"Fujian" +CN,08,"Heilongjiang" +CN,09,"Henan" +CN,10,"Hebei" +CN,11,"Hunan" +CN,12,"Hubei" +CN,13,"Xinjiang" +CN,14,"Xizang" +CN,15,"Gansu" +CN,16,"Guangxi" +CN,18,"Guizhou" +CN,19,"Liaoning" +CN,20,"Nei Mongol" +CN,21,"Ningxia" +CN,22,"Beijing" +CN,23,"Shanghai" +CN,24,"Shanxi" +CN,25,"Shandong" +CN,26,"Shaanxi" +CN,28,"Tianjin" +CN,29,"Yunnan" +CN,30,"Guangdong" +CN,31,"Hainan" +CN,32,"Sichuan" +CN,33,"Chongqing" +CO,01,"Amazonas" +CO,02,"Antioquia" +CO,03,"Arauca" +CO,04,"Atlantico" +CO,08,"Caqueta" +CO,09,"Cauca" +CO,10,"Cesar" +CO,11,"Choco" +CO,12,"Cordoba" +CO,14,"Guaviare" +CO,15,"Guainia" +CO,16,"Huila" +CO,17,"La Guajira" +CO,19,"Meta" +CO,20,"Narino" +CO,21,"Norte de Santander" +CO,22,"Putumayo" +CO,23,"Quindio" +CO,24,"Risaralda" +CO,25,"San Andres y Providencia" +CO,26,"Santander" +CO,27,"Sucre" +CO,28,"Tolima" +CO,29,"Valle del Cauca" +CO,30,"Vaupes" +CO,31,"Vichada" +CO,32,"Casanare" +CO,33,"Cundinamarca" +CO,34,"Distrito Especial" +CO,35,"Bolivar" +CO,36,"Boyaca" +CO,37,"Caldas" +CO,38,"Magdalena" +CR,01,"Alajuela" +CR,02,"Cartago" +CR,03,"Guanacaste" +CR,04,"Heredia" +CR,06,"Limon" +CR,07,"Puntarenas" +CR,08,"San Jose" +CU,01,"Pinar del Rio" +CU,02,"Ciudad de la Habana" +CU,03,"Matanzas" +CU,04,"Isla de la Juventud" +CU,05,"Camaguey" +CU,07,"Ciego de Avila" +CU,08,"Cienfuegos" +CU,09,"Granma" +CU,10,"Guantanamo" +CU,11,"La Habana" +CU,12,"Holguin" +CU,13,"Las Tunas" +CU,14,"Sancti Spiritus" +CU,15,"Santiago de Cuba" +CU,16,"Villa Clara" +CV,01,"Boa Vista" +CV,02,"Brava" +CV,04,"Maio" +CV,05,"Paul" +CV,07,"Ribeira Grande" +CV,08,"Sal" +CV,10,"Sao Nicolau" +CV,11,"Sao Vicente" +CV,13,"Mosteiros" +CV,14,"Praia" +CV,15,"Santa Catarina" +CV,16,"Santa Cruz" +CV,17,"Sao Domingos" +CV,18,"Sao Filipe" +CV,19,"Sao Miguel" +CV,20,"Tarrafal" +CY,01,"Famagusta" +CY,02,"Kyrenia" +CY,03,"Larnaca" +CY,04,"Nicosia" +CY,05,"Limassol" +CY,06,"Paphos" +CZ,52,"Hlavni mesto Praha" +CZ,78,"Jihomoravsky kraj" +CZ,79,"Jihocesky kraj" +CZ,80,"Vysocina" +CZ,81,"Karlovarsky kraj" +CZ,82,"Kralovehradecky kraj" +CZ,83,"Liberecky kraj" +CZ,84,"Olomoucky kraj" +CZ,85,"Moravskoslezsky kraj" +CZ,86,"Pardubicky kraj" +CZ,87,"Plzensky kraj" +CZ,88,"Stredocesky kraj" +CZ,89,"Ustecky kraj" +CZ,90,"Zlinsky kraj" +DE,01,"Baden-Wurttemberg" +DE,02,"Bayern" +DE,03,"Bremen" +DE,04,"Hamburg" +DE,05,"Hessen" +DE,06,"Niedersachsen" +DE,07,"Nordrhein-Westfalen" +DE,08,"Rheinland-Pfalz" +DE,09,"Saarland" +DE,10,"Schleswig-Holstein" +DE,11,"Brandenburg" +DE,12,"Mecklenburg-Vorpommern" +DE,13,"Sachsen" +DE,14,"Sachsen-Anhalt" +DE,15,"Thuringen" +DE,16,"Berlin" +DJ,01,"Ali Sabieh" +DJ,04,"Obock" +DJ,05,"Tadjoura" +DJ,06,"Dikhil" +DJ,07,"Djibouti" +DJ,08,"Arta" +DK,17,"Hovedstaden" +DK,18,"Midtjylland" +DK,19,"Nordjylland" +DK,20,"Sjelland" +DK,21,"Syddanmark" +DM,02,"Saint Andrew" +DM,03,"Saint David" +DM,04,"Saint George" +DM,05,"Saint John" +DM,06,"Saint Joseph" +DM,07,"Saint Luke" +DM,08,"Saint Mark" +DM,09,"Saint Patrick" +DM,10,"Saint Paul" +DM,11,"Saint Peter" +DO,01,"Azua" +DO,02,"Baoruco" +DO,03,"Barahona" +DO,04,"Dajabon" +DO,05,"Distrito Nacional" +DO,06,"Duarte" +DO,08,"Espaillat" +DO,09,"Independencia" +DO,10,"La Altagracia" +DO,11,"Elias Pina" +DO,12,"La Romana" +DO,14,"Maria Trinidad Sanchez" +DO,15,"Monte Cristi" +DO,16,"Pedernales" +DO,17,"Peravia" +DO,18,"Puerto Plata" +DO,19,"Salcedo" +DO,20,"Samana" +DO,21,"Sanchez Ramirez" +DO,23,"San Juan" +DO,24,"San Pedro De Macoris" +DO,25,"Santiago" +DO,26,"Santiago Rodriguez" +DO,27,"Valverde" +DO,28,"El Seibo" +DO,29,"Hato Mayor" +DO,30,"La Vega" +DO,31,"Monsenor Nouel" +DO,32,"Monte Plata" +DO,33,"San Cristobal" +DO,34,"Distrito Nacional" +DO,35,"Peravia" +DO,36,"San Jose de Ocoa" +DO,37,"Santo Domingo" +DZ,01,"Alger" +DZ,03,"Batna" +DZ,04,"Constantine" +DZ,06,"Medea" +DZ,07,"Mostaganem" +DZ,09,"Oran" +DZ,10,"Saida" +DZ,12,"Setif" +DZ,13,"Tiaret" +DZ,14,"Tizi Ouzou" +DZ,15,"Tlemcen" +DZ,18,"Bejaia" +DZ,19,"Biskra" +DZ,20,"Blida" +DZ,21,"Bouira" +DZ,22,"Djelfa" +DZ,23,"Guelma" +DZ,24,"Jijel" +DZ,25,"Laghouat" +DZ,26,"Mascara" +DZ,27,"M'sila" +DZ,29,"Oum el Bouaghi" +DZ,30,"Sidi Bel Abbes" +DZ,31,"Skikda" +DZ,33,"Tebessa" +DZ,34,"Adrar" +DZ,35,"Ain Defla" +DZ,36,"Ain Temouchent" +DZ,37,"Annaba" +DZ,38,"Bechar" +DZ,39,"Bordj Bou Arreridj" +DZ,40,"Boumerdes" +DZ,41,"Chlef" +DZ,42,"El Bayadh" +DZ,43,"El Oued" +DZ,44,"El Tarf" +DZ,45,"Ghardaia" +DZ,46,"Illizi" +DZ,47,"Khenchela" +DZ,48,"Mila" +DZ,49,"Naama" +DZ,50,"Ouargla" +DZ,51,"Relizane" +DZ,52,"Souk Ahras" +DZ,53,"Tamanghasset" +DZ,54,"Tindouf" +DZ,55,"Tipaza" +DZ,56,"Tissemsilt" +EC,01,"Galapagos" +EC,02,"Azuay" +EC,03,"Bolivar" +EC,04,"Canar" +EC,05,"Carchi" +EC,06,"Chimborazo" +EC,07,"Cotopaxi" +EC,08,"El Oro" +EC,09,"Esmeraldas" +EC,10,"Guayas" +EC,11,"Imbabura" +EC,12,"Loja" +EC,13,"Los Rios" +EC,14,"Manabi" +EC,15,"Morona-Santiago" +EC,17,"Pastaza" +EC,18,"Pichincha" +EC,19,"Tungurahua" +EC,20,"Zamora-Chinchipe" +EC,22,"Sucumbios" +EC,23,"Napo" +EC,24,"Orellana" +EE,01,"Harjumaa" +EE,02,"Hiiumaa" +EE,03,"Ida-Virumaa" +EE,04,"Jarvamaa" +EE,05,"Jogevamaa" +EE,06,"Kohtla-Jarve" +EE,07,"Laanemaa" +EE,08,"Laane-Virumaa" +EE,09,"Narva" +EE,10,"Parnu" +EE,11,"Parnumaa" +EE,12,"Polvamaa" +EE,13,"Raplamaa" +EE,14,"Saaremaa" +EE,15,"Sillamae" +EE,16,"Tallinn" +EE,17,"Tartu" +EE,18,"Tartumaa" +EE,19,"Valgamaa" +EE,20,"Viljandimaa" +EE,21,"Vorumaa" +EG,01,"Ad Daqahliyah" +EG,02,"Al Bahr al Ahmar" +EG,03,"Al Buhayrah" +EG,04,"Al Fayyum" +EG,05,"Al Gharbiyah" +EG,06,"Al Iskandariyah" +EG,07,"Al Isma'iliyah" +EG,08,"Al Jizah" +EG,09,"Al Minufiyah" +EG,10,"Al Minya" +EG,11,"Al Qahirah" +EG,12,"Al Qalyubiyah" +EG,13,"Al Wadi al Jadid" +EG,14,"Ash Sharqiyah" +EG,15,"As Suways" +EG,16,"Aswan" +EG,17,"Asyut" +EG,18,"Bani Suwayf" +EG,19,"Bur Sa'id" +EG,20,"Dumyat" +EG,21,"Kafr ash Shaykh" +EG,22,"Matruh" +EG,23,"Qina" +EG,24,"Suhaj" +EG,26,"Janub Sina'" +EG,27,"Shamal Sina'" +EG,28,"Al Uqsur" +ER,01,"Anseba" +ER,02,"Debub" +ER,03,"Debubawi K'eyih Bahri" +ER,04,"Gash Barka" +ER,05,"Ma'akel" +ER,06,"Semenawi K'eyih Bahri" +ES,07,"Islas Baleares" +ES,27,"La Rioja" +ES,29,"Madrid" +ES,31,"Murcia" +ES,32,"Navarra" +ES,34,"Asturias" +ES,39,"Cantabria" +ES,51,"Andalucia" +ES,52,"Aragon" +ES,53,"Canarias" +ES,54,"Castilla-La Mancha" +ES,55,"Castilla y Leon" +ES,56,"Catalonia" +ES,57,"Extremadura" +ES,58,"Galicia" +ES,59,"Pais Vasco" +ES,60,"Comunidad Valenciana" +ET,44,"Adis Abeba" +ET,45,"Afar" +ET,46,"Amara" +ET,47,"Binshangul Gumuz" +ET,48,"Dire Dawa" +ET,49,"Gambela Hizboch" +ET,50,"Hareri Hizb" +ET,51,"Oromiya" +ET,52,"Sumale" +ET,53,"Tigray" +ET,54,"YeDebub Biheroch Bihereseboch na Hizboch" +FI,01,"Aland" +FI,06,"Lapland" +FI,08,"Oulu" +FI,13,"Southern Finland" +FI,14,"Eastern Finland" +FI,15,"Western Finland" +FJ,01,"Central" +FJ,02,"Eastern" +FJ,03,"Northern" +FJ,04,"Rotuma" +FJ,05,"Western" +FM,01,"Kosrae" +FM,02,"Pohnpei" +FM,03,"Chuuk" +FM,04,"Yap" +FR,97,"Aquitaine" +FR,98,"Auvergne" +FR,99,"Basse-Normandie" +FR,A1,"Bourgogne" +FR,A2,"Bretagne" +FR,A3,"Centre" +FR,A4,"Champagne-Ardenne" +FR,A5,"Corse" +FR,A6,"Franche-Comte" +FR,A7,"Haute-Normandie" +FR,A8,"Ile-de-France" +FR,A9,"Languedoc-Roussillon" +FR,B1,"Limousin" +FR,B2,"Lorraine" +FR,B3,"Midi-Pyrenees" +FR,B4,"Nord-Pas-de-Calais" +FR,B5,"Pays de la Loire" +FR,B6,"Picardie" +FR,B7,"Poitou-Charentes" +FR,B8,"Provence-Alpes-Cote d'Azur" +FR,B9,"Rhone-Alpes" +FR,C1,"Alsace" +GA,01,"Estuaire" +GA,02,"Haut-Ogooue" +GA,03,"Moyen-Ogooue" +GA,04,"Ngounie" +GA,05,"Nyanga" +GA,06,"Ogooue-Ivindo" +GA,07,"Ogooue-Lolo" +GA,08,"Ogooue-Maritime" +GA,09,"Woleu-Ntem" +GB,A1,"Barking and Dagenham" +GB,A2,"Barnet" +GB,A3,"Barnsley" +GB,A4,"Bath and North East Somerset" +GB,A5,"Bedfordshire" +GB,A6,"Bexley" +GB,A7,"Birmingham" +GB,A8,"Blackburn with Darwen" +GB,A9,"Blackpool" +GB,B1,"Bolton" +GB,B2,"Bournemouth" +GB,B3,"Bracknell Forest" +GB,B4,"Bradford" +GB,B5,"Brent" +GB,B6,"Brighton and Hove" +GB,B7,"Bristol, City of" +GB,B8,"Bromley" +GB,B9,"Buckinghamshire" +GB,C1,"Bury" +GB,C2,"Calderdale" +GB,C3,"Cambridgeshire" +GB,C4,"Camden" +GB,C5,"Cheshire" +GB,C6,"Cornwall" +GB,C7,"Coventry" +GB,C8,"Croydon" +GB,C9,"Cumbria" +GB,D1,"Darlington" +GB,D2,"Derby" +GB,D3,"Derbyshire" +GB,D4,"Devon" +GB,D5,"Doncaster" +GB,D6,"Dorset" +GB,D7,"Dudley" +GB,D8,"Durham" +GB,D9,"Ealing" +GB,E1,"East Riding of Yorkshire" +GB,E2,"East Sussex" +GB,E3,"Enfield" +GB,E4,"Essex" +GB,E5,"Gateshead" +GB,E6,"Gloucestershire" +GB,E7,"Greenwich" +GB,E8,"Hackney" +GB,E9,"Halton" +GB,F1,"Hammersmith and Fulham" +GB,F2,"Hampshire" +GB,F3,"Haringey" +GB,F4,"Harrow" +GB,F5,"Hartlepool" +GB,F6,"Havering" +GB,F7,"Herefordshire" +GB,F8,"Hertford" +GB,F9,"Hillingdon" +GB,G1,"Hounslow" +GB,G2,"Isle of Wight" +GB,G3,"Islington" +GB,G4,"Kensington and Chelsea" +GB,G5,"Kent" +GB,G6,"Kingston upon Hull, City of" +GB,G7,"Kingston upon Thames" +GB,G8,"Kirklees" +GB,G9,"Knowsley" +GB,H1,"Lambeth" +GB,H2,"Lancashire" +GB,H3,"Leeds" +GB,H4,"Leicester" +GB,H5,"Leicestershire" +GB,H6,"Lewisham" +GB,H7,"Lincolnshire" +GB,H8,"Liverpool" +GB,H9,"London, City of" +GB,I1,"Luton" +GB,I2,"Manchester" +GB,I3,"Medway" +GB,I4,"Merton" +GB,I5,"Middlesbrough" +GB,I6,"Milton Keynes" +GB,I7,"Newcastle upon Tyne" +GB,I8,"Newham" +GB,I9,"Norfolk" +GB,J1,"Northamptonshire" +GB,J2,"North East Lincolnshire" +GB,J3,"North Lincolnshire" +GB,J4,"North Somerset" +GB,J5,"North Tyneside" +GB,J6,"Northumberland" +GB,J7,"North Yorkshire" +GB,J8,"Nottingham" +GB,J9,"Nottinghamshire" +GB,K1,"Oldham" +GB,K2,"Oxfordshire" +GB,K3,"Peterborough" +GB,K4,"Plymouth" +GB,K5,"Poole" +GB,K6,"Portsmouth" +GB,K7,"Reading" +GB,K8,"Redbridge" +GB,K9,"Redcar and Cleveland" +GB,L1,"Richmond upon Thames" +GB,L2,"Rochdale" +GB,L3,"Rotherham" +GB,L4,"Rutland" +GB,L5,"Salford" +GB,L6,"Shropshire" +GB,L7,"Sandwell" +GB,L8,"Sefton" +GB,L9,"Sheffield" +GB,M1,"Slough" +GB,M2,"Solihull" +GB,M3,"Somerset" +GB,M4,"Southampton" +GB,M5,"Southend-on-Sea" +GB,M6,"South Gloucestershire" +GB,M7,"South Tyneside" +GB,M8,"Southwark" +GB,M9,"Staffordshire" +GB,N1,"St. Helens" +GB,N2,"Stockport" +GB,N3,"Stockton-on-Tees" +GB,N4,"Stoke-on-Trent" +GB,N5,"Suffolk" +GB,N6,"Sunderland" +GB,N7,"Surrey" +GB,N8,"Sutton" +GB,N9,"Swindon" +GB,O1,"Tameside" +GB,O2,"Telford and Wrekin" +GB,O3,"Thurrock" +GB,O4,"Torbay" +GB,O5,"Tower Hamlets" +GB,O6,"Trafford" +GB,O7,"Wakefield" +GB,O8,"Walsall" +GB,O9,"Waltham Forest" +GB,P1,"Wandsworth" +GB,P2,"Warrington" +GB,P3,"Warwickshire" +GB,P4,"West Berkshire" +GB,P5,"Westminster" +GB,P6,"West Sussex" +GB,P7,"Wigan" +GB,P8,"Wiltshire" +GB,P9,"Windsor and Maidenhead" +GB,Q1,"Wirral" +GB,Q2,"Wokingham" +GB,Q3,"Wolverhampton" +GB,Q4,"Worcestershire" +GB,Q5,"York" +GB,Q6,"Antrim" +GB,Q7,"Ards" +GB,Q8,"Armagh" +GB,Q9,"Ballymena" +GB,R1,"Ballymoney" +GB,R2,"Banbridge" +GB,R3,"Belfast" +GB,R4,"Carrickfergus" +GB,R5,"Castlereagh" +GB,R6,"Coleraine" +GB,R7,"Cookstown" +GB,R8,"Craigavon" +GB,R9,"Down" +GB,S1,"Dungannon" +GB,S2,"Fermanagh" +GB,S3,"Larne" +GB,S4,"Limavady" +GB,S5,"Lisburn" +GB,S6,"Derry" +GB,S7,"Magherafelt" +GB,S8,"Moyle" +GB,S9,"Newry and Mourne" +GB,T1,"Newtownabbey" +GB,T2,"North Down" +GB,T3,"Omagh" +GB,T4,"Strabane" +GB,T5,"Aberdeen City" +GB,T6,"Aberdeenshire" +GB,T7,"Angus" +GB,T8,"Argyll and Bute" +GB,T9,"Scottish Borders, The" +GB,U1,"Clackmannanshire" +GB,U2,"Dumfries and Galloway" +GB,U3,"Dundee City" +GB,U4,"East Ayrshire" +GB,U5,"East Dunbartonshire" +GB,U6,"East Lothian" +GB,U7,"East Renfrewshire" +GB,U8,"Edinburgh, City of" +GB,U9,"Falkirk" +GB,V1,"Fife" +GB,V2,"Glasgow City" +GB,V3,"Highland" +GB,V4,"Inverclyde" +GB,V5,"Midlothian" +GB,V6,"Moray" +GB,V7,"North Ayrshire" +GB,V8,"North Lanarkshire" +GB,V9,"Orkney" +GB,W1,"Perth and Kinross" +GB,W2,"Renfrewshire" +GB,W3,"Shetland Islands" +GB,W4,"South Ayrshire" +GB,W5,"South Lanarkshire" +GB,W6,"Stirling" +GB,W7,"West Dunbartonshire" +GB,W8,"Eilean Siar" +GB,W9,"West Lothian" +GB,X1,"Isle of Anglesey" +GB,X2,"Blaenau Gwent" +GB,X3,"Bridgend" +GB,X4,"Caerphilly" +GB,X5,"Cardiff" +GB,X6,"Ceredigion" +GB,X7,"Carmarthenshire" +GB,X8,"Conwy" +GB,X9,"Denbighshire" +GB,Y1,"Flintshire" +GB,Y2,"Gwynedd" +GB,Y3,"Merthyr Tydfil" +GB,Y4,"Monmouthshire" +GB,Y5,"Neath Port Talbot" +GB,Y6,"Newport" +GB,Y7,"Pembrokeshire" +GB,Y8,"Powys" +GB,Y9,"Rhondda Cynon Taff" +GB,Z1,"Swansea" +GB,Z2,"Torfaen" +GB,Z3,"Vale of Glamorgan, The" +GB,Z4,"Wrexham" +GB,Z5,"Bedfordshire" +GB,Z6,"Central Bedfordshire" +GB,Z7,"Cheshire East" +GB,Z8,"Cheshire West and Chester" +GB,Z9,"Isles of Scilly" +GD,01,"Saint Andrew" +GD,02,"Saint David" +GD,03,"Saint George" +GD,04,"Saint John" +GD,05,"Saint Mark" +GD,06,"Saint Patrick" +GE,01,"Abashis Raioni" +GE,02,"Abkhazia" +GE,03,"Adigenis Raioni" +GE,04,"Ajaria" +GE,05,"Akhalgoris Raioni" +GE,06,"Akhalk'alak'is Raioni" +GE,07,"Akhalts'ikhis Raioni" +GE,08,"Akhmetis Raioni" +GE,09,"Ambrolauris Raioni" +GE,10,"Aspindzis Raioni" +GE,11,"Baghdat'is Raioni" +GE,12,"Bolnisis Raioni" +GE,13,"Borjomis Raioni" +GE,14,"Chiat'ura" +GE,15,"Ch'khorotsqus Raioni" +GE,16,"Ch'okhatauris Raioni" +GE,17,"Dedop'listsqaros Raioni" +GE,18,"Dmanisis Raioni" +GE,19,"Dushet'is Raioni" +GE,20,"Gardabanis Raioni" +GE,21,"Gori" +GE,22,"Goris Raioni" +GE,23,"Gurjaanis Raioni" +GE,24,"Javis Raioni" +GE,25,"K'arelis Raioni" +GE,26,"Kaspis Raioni" +GE,27,"Kharagaulis Raioni" +GE,28,"Khashuris Raioni" +GE,29,"Khobis Raioni" +GE,30,"Khonis Raioni" +GE,31,"K'ut'aisi" +GE,32,"Lagodekhis Raioni" +GE,33,"Lanch'khut'is Raioni" +GE,34,"Lentekhis Raioni" +GE,35,"Marneulis Raioni" +GE,36,"Martvilis Raioni" +GE,37,"Mestiis Raioni" +GE,38,"Mts'khet'is Raioni" +GE,39,"Ninotsmindis Raioni" +GE,40,"Onis Raioni" +GE,41,"Ozurget'is Raioni" +GE,42,"P'ot'i" +GE,43,"Qazbegis Raioni" +GE,44,"Qvarlis Raioni" +GE,45,"Rust'avi" +GE,46,"Sach'kheris Raioni" +GE,47,"Sagarejos Raioni" +GE,48,"Samtrediis Raioni" +GE,49,"Senakis Raioni" +GE,50,"Sighnaghis Raioni" +GE,51,"T'bilisi" +GE,52,"T'elavis Raioni" +GE,53,"T'erjolis Raioni" +GE,54,"T'et'ritsqaros Raioni" +GE,55,"T'ianet'is Raioni" +GE,56,"Tqibuli" +GE,57,"Ts'ageris Raioni" +GE,58,"Tsalenjikhis Raioni" +GE,59,"Tsalkis Raioni" +GE,60,"Tsqaltubo" +GE,61,"Vanis Raioni" +GE,62,"Zestap'onis Raioni" +GE,63,"Zugdidi" +GE,64,"Zugdidis Raioni" +GH,01,"Greater Accra" +GH,02,"Ashanti" +GH,03,"Brong-Ahafo" +GH,04,"Central" +GH,05,"Eastern" +GH,06,"Northern" +GH,08,"Volta" +GH,09,"Western" +GH,10,"Upper East" +GH,11,"Upper West" +GL,01,"Nordgronland" +GL,02,"Ostgronland" +GL,03,"Vestgronland" +GM,01,"Banjul" +GM,02,"Lower River" +GM,03,"Central River" +GM,04,"Upper River" +GM,05,"Western" +GM,07,"North Bank" +GN,01,"Beyla" +GN,02,"Boffa" +GN,03,"Boke" +GN,04,"Conakry" +GN,05,"Dabola" +GN,06,"Dalaba" +GN,07,"Dinguiraye" +GN,09,"Faranah" +GN,10,"Forecariah" +GN,11,"Fria" +GN,12,"Gaoual" +GN,13,"Gueckedou" +GN,15,"Kerouane" +GN,16,"Kindia" +GN,17,"Kissidougou" +GN,18,"Koundara" +GN,19,"Kouroussa" +GN,21,"Macenta" +GN,22,"Mali" +GN,23,"Mamou" +GN,25,"Pita" +GN,27,"Telimele" +GN,28,"Tougue" +GN,29,"Yomou" +GN,30,"Coyah" +GN,31,"Dubreka" +GN,32,"Kankan" +GN,33,"Koubia" +GN,34,"Labe" +GN,35,"Lelouma" +GN,36,"Lola" +GN,37,"Mandiana" +GN,38,"Nzerekore" +GN,39,"Siguiri" +GQ,03,"Annobon" +GQ,04,"Bioko Norte" +GQ,05,"Bioko Sur" +GQ,06,"Centro Sur" +GQ,07,"Kie-Ntem" +GQ,08,"Litoral" +GQ,09,"Wele-Nzas" +GR,01,"Evros" +GR,02,"Rodhopi" +GR,03,"Xanthi" +GR,04,"Drama" +GR,05,"Serrai" +GR,06,"Kilkis" +GR,07,"Pella" +GR,08,"Florina" +GR,09,"Kastoria" +GR,10,"Grevena" +GR,11,"Kozani" +GR,12,"Imathia" +GR,13,"Thessaloniki" +GR,14,"Kavala" +GR,15,"Khalkidhiki" +GR,16,"Pieria" +GR,17,"Ioannina" +GR,18,"Thesprotia" +GR,19,"Preveza" +GR,20,"Arta" +GR,21,"Larisa" +GR,22,"Trikala" +GR,23,"Kardhitsa" +GR,24,"Magnisia" +GR,25,"Kerkira" +GR,26,"Levkas" +GR,27,"Kefallinia" +GR,28,"Zakinthos" +GR,29,"Fthiotis" +GR,30,"Evritania" +GR,31,"Aitolia kai Akarnania" +GR,32,"Fokis" +GR,33,"Voiotia" +GR,34,"Evvoia" +GR,35,"Attiki" +GR,36,"Argolis" +GR,37,"Korinthia" +GR,38,"Akhaia" +GR,39,"Ilia" +GR,40,"Messinia" +GR,41,"Arkadhia" +GR,42,"Lakonia" +GR,43,"Khania" +GR,44,"Rethimni" +GR,45,"Iraklion" +GR,46,"Lasithi" +GR,47,"Dhodhekanisos" +GR,48,"Samos" +GR,49,"Kikladhes" +GR,50,"Khios" +GR,51,"Lesvos" +GT,01,"Alta Verapaz" +GT,02,"Baja Verapaz" +GT,03,"Chimaltenango" +GT,04,"Chiquimula" +GT,05,"El Progreso" +GT,06,"Escuintla" +GT,07,"Guatemala" +GT,08,"Huehuetenango" +GT,09,"Izabal" +GT,10,"Jalapa" +GT,11,"Jutiapa" +GT,12,"Peten" +GT,13,"Quetzaltenango" +GT,14,"Quiche" +GT,15,"Retalhuleu" +GT,16,"Sacatepequez" +GT,17,"San Marcos" +GT,18,"Santa Rosa" +GT,19,"Solola" +GT,20,"Suchitepequez" +GT,21,"Totonicapan" +GT,22,"Zacapa" +GW,01,"Bafata" +GW,02,"Quinara" +GW,04,"Oio" +GW,05,"Bolama" +GW,06,"Cacheu" +GW,07,"Tombali" +GW,10,"Gabu" +GW,11,"Bissau" +GW,12,"Biombo" +GY,10,"Barima-Waini" +GY,11,"Cuyuni-Mazaruni" +GY,12,"Demerara-Mahaica" +GY,13,"East Berbice-Corentyne" +GY,14,"Essequibo Islands-West Demerara" +GY,15,"Mahaica-Berbice" +GY,16,"Pomeroon-Supenaam" +GY,17,"Potaro-Siparuni" +GY,18,"Upper Demerara-Berbice" +GY,19,"Upper Takutu-Upper Essequibo" +HN,01,"Atlantida" +HN,02,"Choluteca" +HN,03,"Colon" +HN,04,"Comayagua" +HN,05,"Copan" +HN,06,"Cortes" +HN,07,"El Paraiso" +HN,08,"Francisco Morazan" +HN,09,"Gracias a Dios" +HN,10,"Intibuca" +HN,11,"Islas de la Bahia" +HN,12,"La Paz" +HN,13,"Lempira" +HN,14,"Ocotepeque" +HN,15,"Olancho" +HN,16,"Santa Barbara" +HN,17,"Valle" +HN,18,"Yoro" +HR,01,"Bjelovarsko-Bilogorska" +HR,02,"Brodsko-Posavska" +HR,03,"Dubrovacko-Neretvanska" +HR,04,"Istarska" +HR,05,"Karlovacka" +HR,06,"Koprivnicko-Krizevacka" +HR,07,"Krapinsko-Zagorska" +HR,08,"Licko-Senjska" +HR,09,"Medimurska" +HR,10,"Osjecko-Baranjska" +HR,11,"Pozesko-Slavonska" +HR,12,"Primorsko-Goranska" +HR,13,"Sibensko-Kninska" +HR,14,"Sisacko-Moslavacka" +HR,15,"Splitsko-Dalmatinska" +HR,16,"Varazdinska" +HR,17,"Viroviticko-Podravska" +HR,18,"Vukovarsko-Srijemska" +HR,19,"Zadarska" +HR,20,"Zagrebacka" +HR,21,"Grad Zagreb" +HT,03,"Nord-Ouest" +HT,06,"Artibonite" +HT,07,"Centre" +HT,09,"Nord" +HT,10,"Nord-Est" +HT,11,"Ouest" +HT,12,"Sud" +HT,13,"Sud-Est" +HT,14,"Grand' Anse" +HT,15,"Nippes" +HU,01,"Bacs-Kiskun" +HU,02,"Baranya" +HU,03,"Bekes" +HU,04,"Borsod-Abauj-Zemplen" +HU,05,"Budapest" +HU,06,"Csongrad" +HU,07,"Debrecen" +HU,08,"Fejer" +HU,09,"Gyor-Moson-Sopron" +HU,10,"Hajdu-Bihar" +HU,11,"Heves" +HU,12,"Komarom-Esztergom" +HU,13,"Miskolc" +HU,14,"Nograd" +HU,15,"Pecs" +HU,16,"Pest" +HU,17,"Somogy" +HU,18,"Szabolcs-Szatmar-Bereg" +HU,19,"Szeged" +HU,20,"Jasz-Nagykun-Szolnok" +HU,21,"Tolna" +HU,22,"Vas" +HU,23,"Veszprem" +HU,24,"Zala" +HU,25,"Gyor" +HU,26,"Bekescsaba" +HU,27,"Dunaujvaros" +HU,28,"Eger" +HU,29,"Hodmezovasarhely" +HU,30,"Kaposvar" +HU,31,"Kecskemet" +HU,32,"Nagykanizsa" +HU,33,"Nyiregyhaza" +HU,34,"Sopron" +HU,35,"Szekesfehervar" +HU,36,"Szolnok" +HU,37,"Szombathely" +HU,38,"Tatabanya" +HU,39,"Veszprem" +HU,40,"Zalaegerszeg" +HU,41,"Salgotarjan" +HU,42,"Szekszard" +HU,43,"Erd" +ID,01,"Aceh" +ID,02,"Bali" +ID,03,"Bengkulu" +ID,04,"Jakarta Raya" +ID,05,"Jambi" +ID,07,"Jawa Tengah" +ID,08,"Jawa Timur" +ID,10,"Yogyakarta" +ID,11,"Kalimantan Barat" +ID,12,"Kalimantan Selatan" +ID,13,"Kalimantan Tengah" +ID,14,"Kalimantan Timur" +ID,15,"Lampung" +ID,17,"Nusa Tenggara Barat" +ID,18,"Nusa Tenggara Timur" +ID,21,"Sulawesi Tengah" +ID,22,"Sulawesi Tenggara" +ID,24,"Sumatera Barat" +ID,26,"Sumatera Utara" +ID,28,"Maluku" +ID,29,"Maluku Utara" +ID,30,"Jawa Barat" +ID,31,"Sulawesi Utara" +ID,32,"Sumatera Selatan" +ID,33,"Banten" +ID,34,"Gorontalo" +ID,35,"Kepulauan Bangka Belitung" +ID,36,"Papua" +ID,37,"Riau" +ID,38,"Sulawesi Selatan" +ID,39,"Irian Jaya Barat" +ID,40,"Kepulauan Riau" +ID,41,"Sulawesi Barat" +IE,01,"Carlow" +IE,02,"Cavan" +IE,03,"Clare" +IE,04,"Cork" +IE,06,"Donegal" +IE,07,"Dublin" +IE,10,"Galway" +IE,11,"Kerry" +IE,12,"Kildare" +IE,13,"Kilkenny" +IE,14,"Leitrim" +IE,15,"Laois" +IE,16,"Limerick" +IE,18,"Longford" +IE,19,"Louth" +IE,20,"Mayo" +IE,21,"Meath" +IE,22,"Monaghan" +IE,23,"Offaly" +IE,24,"Roscommon" +IE,25,"Sligo" +IE,26,"Tipperary" +IE,27,"Waterford" +IE,29,"Westmeath" +IE,30,"Wexford" +IE,31,"Wicklow" +IL,01,"HaDarom" +IL,02,"HaMerkaz" +IL,03,"HaZafon" +IL,04,"Hefa" +IL,05,"Tel Aviv" +IL,06,"Yerushalayim" +IN,01,"Andaman and Nicobar Islands" +IN,02,"Andhra Pradesh" +IN,03,"Assam" +IN,05,"Chandigarh" +IN,06,"Dadra and Nagar Haveli" +IN,07,"Delhi" +IN,09,"Gujarat" +IN,10,"Haryana" +IN,11,"Himachal Pradesh" +IN,12,"Jammu and Kashmir" +IN,13,"Kerala" +IN,14,"Lakshadweep" +IN,16,"Maharashtra" +IN,17,"Manipur" +IN,18,"Meghalaya" +IN,19,"Karnataka" +IN,20,"Nagaland" +IN,21,"Orissa" +IN,22,"Puducherry" +IN,23,"Punjab" +IN,24,"Rajasthan" +IN,25,"Tamil Nadu" +IN,26,"Tripura" +IN,28,"West Bengal" +IN,29,"Sikkim" +IN,30,"Arunachal Pradesh" +IN,31,"Mizoram" +IN,32,"Daman and Diu" +IN,33,"Goa" +IN,34,"Bihar" +IN,35,"Madhya Pradesh" +IN,36,"Uttar Pradesh" +IN,37,"Chhattisgarh" +IN,38,"Jharkhand" +IN,39,"Uttarakhand" +IQ,01,"Al Anbar" +IQ,02,"Al Basrah" +IQ,03,"Al Muthanna" +IQ,04,"Al Qadisiyah" +IQ,05,"As Sulaymaniyah" +IQ,06,"Babil" +IQ,07,"Baghdad" +IQ,08,"Dahuk" +IQ,09,"Dhi Qar" +IQ,10,"Diyala" +IQ,11,"Arbil" +IQ,12,"Karbala'" +IQ,13,"At Ta'mim" +IQ,14,"Maysan" +IQ,15,"Ninawa" +IQ,16,"Wasit" +IQ,17,"An Najaf" +IQ,18,"Salah ad Din" +IR,01,"Azarbayjan-e Bakhtari" +IR,03,"Chahar Mahall va Bakhtiari" +IR,04,"Sistan va Baluchestan" +IR,05,"Kohkiluyeh va Buyer Ahmadi" +IR,07,"Fars" +IR,08,"Gilan" +IR,09,"Hamadan" +IR,10,"Ilam" +IR,11,"Hormozgan" +IR,12,"Kerman" +IR,13,"Bakhtaran" +IR,15,"Khuzestan" +IR,16,"Kordestan" +IR,17,"Mazandaran" +IR,18,"Semnan Province" +IR,19,"Markazi" +IR,21,"Zanjan" +IR,22,"Bushehr" +IR,23,"Lorestan" +IR,24,"Markazi" +IR,25,"Semnan" +IR,26,"Tehran" +IR,27,"Zanjan" +IR,28,"Esfahan" +IR,29,"Kerman" +IR,30,"Khorasan" +IR,31,"Yazd" +IR,32,"Ardabil" +IR,33,"East Azarbaijan" +IR,34,"Markazi" +IR,35,"Mazandaran" +IR,36,"Zanjan" +IR,37,"Golestan" +IR,38,"Qazvin" +IR,39,"Qom" +IR,40,"Yazd" +IR,41,"Khorasan-e Janubi" +IR,42,"Khorasan-e Razavi" +IR,43,"Khorasan-e Shemali" +IR,44,"Alborz" +IS,03,"Arnessysla" +IS,05,"Austur-Hunavatnssysla" +IS,06,"Austur-Skaftafellssysla" +IS,07,"Borgarfjardarsysla" +IS,09,"Eyjafjardarsysla" +IS,10,"Gullbringusysla" +IS,15,"Kjosarsysla" +IS,17,"Myrasysla" +IS,20,"Nordur-Mulasysla" +IS,21,"Nordur-Tingeyjarsysla" +IS,23,"Rangarvallasysla" +IS,28,"Skagafjardarsysla" +IS,29,"Snafellsnes- og Hnappadalssysla" +IS,30,"Strandasysla" +IS,31,"Sudur-Mulasysla" +IS,32,"Sudur-Tingeyjarsysla" +IS,34,"Vestur-Bardastrandarsysla" +IS,35,"Vestur-Hunavatnssysla" +IS,36,"Vestur-Isafjardarsysla" +IS,37,"Vestur-Skaftafellssysla" +IS,38,"Austurland" +IS,39,"Hofuoborgarsvaoio" +IS,40,"Norourland Eystra" +IS,41,"Norourland Vestra" +IS,42,"Suourland" +IS,43,"Suournes" +IS,44,"Vestfiroir" +IS,45,"Vesturland" +IT,01,"Abruzzi" +IT,02,"Basilicata" +IT,03,"Calabria" +IT,04,"Campania" +IT,05,"Emilia-Romagna" +IT,06,"Friuli-Venezia Giulia" +IT,07,"Lazio" +IT,08,"Liguria" +IT,09,"Lombardia" +IT,10,"Marche" +IT,11,"Molise" +IT,12,"Piemonte" +IT,13,"Puglia" +IT,14,"Sardegna" +IT,15,"Sicilia" +IT,16,"Toscana" +IT,17,"Trentino-Alto Adige" +IT,18,"Umbria" +IT,19,"Valle d'Aosta" +IT,20,"Veneto" +JM,01,"Clarendon" +JM,02,"Hanover" +JM,04,"Manchester" +JM,07,"Portland" +JM,08,"Saint Andrew" +JM,09,"Saint Ann" +JM,10,"Saint Catherine" +JM,11,"Saint Elizabeth" +JM,12,"Saint James" +JM,13,"Saint Mary" +JM,14,"Saint Thomas" +JM,15,"Trelawny" +JM,16,"Westmoreland" +JM,17,"Kingston" +JO,02,"Al Balqa'" +JO,09,"Al Karak" +JO,12,"At Tafilah" +JO,15,"Al Mafraq" +JO,16,"Amman" +JO,17,"Az Zaraqa" +JO,18,"Irbid" +JO,19,"Ma'an" +JO,20,"Ajlun" +JO,21,"Al Aqabah" +JO,22,"Jarash" +JO,23,"Madaba" +JP,01,"Aichi" +JP,02,"Akita" +JP,03,"Aomori" +JP,04,"Chiba" +JP,05,"Ehime" +JP,06,"Fukui" +JP,07,"Fukuoka" +JP,08,"Fukushima" +JP,09,"Gifu" +JP,10,"Gumma" +JP,11,"Hiroshima" +JP,12,"Hokkaido" +JP,13,"Hyogo" +JP,14,"Ibaraki" +JP,15,"Ishikawa" +JP,16,"Iwate" +JP,17,"Kagawa" +JP,18,"Kagoshima" +JP,19,"Kanagawa" +JP,20,"Kochi" +JP,21,"Kumamoto" +JP,22,"Kyoto" +JP,23,"Mie" +JP,24,"Miyagi" +JP,25,"Miyazaki" +JP,26,"Nagano" +JP,27,"Nagasaki" +JP,28,"Nara" +JP,29,"Niigata" +JP,30,"Oita" +JP,31,"Okayama" +JP,32,"Osaka" +JP,33,"Saga" +JP,34,"Saitama" +JP,35,"Shiga" +JP,36,"Shimane" +JP,37,"Shizuoka" +JP,38,"Tochigi" +JP,39,"Tokushima" +JP,40,"Tokyo" +JP,41,"Tottori" +JP,42,"Toyama" +JP,43,"Wakayama" +JP,44,"Yamagata" +JP,45,"Yamaguchi" +JP,46,"Yamanashi" +JP,47,"Okinawa" +KE,01,"Central" +KE,02,"Coast" +KE,03,"Eastern" +KE,05,"Nairobi Area" +KE,06,"North-Eastern" +KE,07,"Nyanza" +KE,08,"Rift Valley" +KE,09,"Western" +KG,01,"Bishkek" +KG,02,"Chuy" +KG,03,"Jalal-Abad" +KG,04,"Naryn" +KG,05,"Osh" +KG,06,"Talas" +KG,07,"Ysyk-Kol" +KG,08,"Osh" +KG,09,"Batken" +KH,01,"Batdambang" +KH,02,"Kampong Cham" +KH,03,"Kampong Chhnang" +KH,04,"Kampong Speu" +KH,05,"Kampong Thum" +KH,06,"Kampot" +KH,07,"Kandal" +KH,08,"Koh Kong" +KH,09,"Kracheh" +KH,10,"Mondulkiri" +KH,11,"Phnum Penh" +KH,12,"Pursat" +KH,13,"Preah Vihear" +KH,14,"Prey Veng" +KH,15,"Ratanakiri Kiri" +KH,16,"Siem Reap" +KH,17,"Stung Treng" +KH,18,"Svay Rieng" +KH,19,"Takeo" +KH,22,"Phnum Penh" +KH,25,"Banteay Meanchey" +KH,28,"Preah Seihanu" +KH,29,"Batdambang" +KH,30,"Pailin" +KI,01,"Gilbert Islands" +KI,02,"Line Islands" +KI,03,"Phoenix Islands" +KM,01,"Anjouan" +KM,02,"Grande Comore" +KM,03,"Moheli" +KN,01,"Christ Church Nichola Town" +KN,02,"Saint Anne Sandy Point" +KN,03,"Saint George Basseterre" +KN,04,"Saint George Gingerland" +KN,05,"Saint James Windward" +KN,06,"Saint John Capisterre" +KN,07,"Saint John Figtree" +KN,08,"Saint Mary Cayon" +KN,09,"Saint Paul Capisterre" +KN,10,"Saint Paul Charlestown" +KN,11,"Saint Peter Basseterre" +KN,12,"Saint Thomas Lowland" +KN,13,"Saint Thomas Middle Island" +KN,15,"Trinity Palmetto Point" +KP,01,"Chagang-do" +KP,03,"Hamgyong-namdo" +KP,06,"Hwanghae-namdo" +KP,07,"Hwanghae-bukto" +KP,08,"Kaesong-si" +KP,09,"Kangwon-do" +KP,11,"P'yongan-bukto" +KP,12,"P'yongyang-si" +KP,13,"Yanggang-do" +KP,14,"Namp'o-si" +KP,15,"P'yongan-namdo" +KP,17,"Hamgyong-bukto" +KP,18,"Najin Sonbong-si" +KR,01,"Cheju-do" +KR,03,"Cholla-bukto" +KR,05,"Ch'ungch'ong-bukto" +KR,06,"Kangwon-do" +KR,10,"Pusan-jikhalsi" +KR,11,"Seoul-t'ukpyolsi" +KR,12,"Inch'on-jikhalsi" +KR,13,"Kyonggi-do" +KR,14,"Kyongsang-bukto" +KR,15,"Taegu-jikhalsi" +KR,16,"Cholla-namdo" +KR,17,"Ch'ungch'ong-namdo" +KR,18,"Kwangju-jikhalsi" +KR,19,"Taejon-jikhalsi" +KR,20,"Kyongsang-namdo" +KR,21,"Ulsan-gwangyoksi" +KW,01,"Al Ahmadi" +KW,02,"Al Kuwayt" +KW,05,"Al Jahra" +KW,07,"Al Farwaniyah" +KW,08,"Hawalli" +KW,09,"Mubarak al Kabir" +KY,01,"Creek" +KY,02,"Eastern" +KY,03,"Midland" +KY,04,"South Town" +KY,05,"Spot Bay" +KY,06,"Stake Bay" +KY,07,"West End" +KY,08,"Western" +KZ,01,"Almaty" +KZ,02,"Almaty City" +KZ,03,"Aqmola" +KZ,04,"Aqtobe" +KZ,05,"Astana" +KZ,06,"Atyrau" +KZ,07,"West Kazakhstan" +KZ,08,"Bayqonyr" +KZ,09,"Mangghystau" +KZ,10,"South Kazakhstan" +KZ,11,"Pavlodar" +KZ,12,"Qaraghandy" +KZ,13,"Qostanay" +KZ,14,"Qyzylorda" +KZ,15,"East Kazakhstan" +KZ,16,"North Kazakhstan" +KZ,17,"Zhambyl" +LA,01,"Attapu" +LA,02,"Champasak" +LA,03,"Houaphan" +LA,04,"Khammouan" +LA,05,"Louang Namtha" +LA,07,"Oudomxai" +LA,08,"Phongsali" +LA,09,"Saravan" +LA,10,"Savannakhet" +LA,11,"Vientiane" +LA,13,"Xaignabouri" +LA,14,"Xiangkhoang" +LA,17,"Louangphrabang" +LB,01,"Beqaa" +LB,02,"Al Janub" +LB,03,"Liban-Nord" +LB,04,"Beyrouth" +LB,05,"Mont-Liban" +LB,06,"Liban-Sud" +LB,07,"Nabatiye" +LB,08,"Beqaa" +LB,09,"Liban-Nord" +LB,10,"Aakk,r" +LB,11,"Baalbek-Hermel" +LC,01,"Anse-la-Raye" +LC,02,"Dauphin" +LC,03,"Castries" +LC,04,"Choiseul" +LC,05,"Dennery" +LC,06,"Gros-Islet" +LC,07,"Laborie" +LC,08,"Micoud" +LC,09,"Soufriere" +LC,10,"Vieux-Fort" +LC,11,"Praslin" +LI,01,"Balzers" +LI,02,"Eschen" +LI,03,"Gamprin" +LI,04,"Mauren" +LI,05,"Planken" +LI,06,"Ruggell" +LI,07,"Schaan" +LI,08,"Schellenberg" +LI,09,"Triesen" +LI,10,"Triesenberg" +LI,11,"Vaduz" +LI,21,"Gbarpolu" +LI,22,"River Gee" +LK,29,"Central" +LK,30,"North Central" +LK,32,"North Western" +LK,33,"Sabaragamuwa" +LK,34,"Southern" +LK,35,"Uva" +LK,36,"Western" +LK,37,"Eastern" +LK,38,"Northern" +LR,01,"Bong" +LR,04,"Grand Cape Mount" +LR,05,"Lofa" +LR,06,"Maryland" +LR,07,"Monrovia" +LR,09,"Nimba" +LR,10,"Sino" +LR,11,"Grand Bassa" +LR,12,"Grand Cape Mount" +LR,13,"Maryland" +LR,14,"Montserrado" +LR,17,"Margibi" +LR,18,"River Cess" +LR,19,"Grand Gedeh" +LR,20,"Lofa" +LR,21,"Gbarpolu" +LR,22,"River Gee" +LS,10,"Berea" +LS,11,"Butha-Buthe" +LS,12,"Leribe" +LS,13,"Mafeteng" +LS,14,"Maseru" +LS,15,"Mohales Hoek" +LS,16,"Mokhotlong" +LS,17,"Qachas Nek" +LS,18,"Quthing" +LS,19,"Thaba-Tseka" +LT,56,"Alytaus Apskritis" +LT,57,"Kauno Apskritis" +LT,58,"Klaipedos Apskritis" +LT,59,"Marijampoles Apskritis" +LT,60,"Panevezio Apskritis" +LT,61,"Siauliu Apskritis" +LT,62,"Taurages Apskritis" +LT,63,"Telsiu Apskritis" +LT,64,"Utenos Apskritis" +LT,65,"Vilniaus Apskritis" +LU,01,"Diekirch" +LU,02,"Grevenmacher" +LU,03,"Luxembourg" +LV,01,"Aizkraukles" +LV,02,"Aluksnes" +LV,03,"Balvu" +LV,04,"Bauskas" +LV,05,"Cesu" +LV,06,"Daugavpils" +LV,07,"Daugavpils" +LV,08,"Dobeles" +LV,09,"Gulbenes" +LV,10,"Jekabpils" +LV,11,"Jelgava" +LV,12,"Jelgavas" +LV,13,"Jurmala" +LV,14,"Kraslavas" +LV,15,"Kuldigas" +LV,16,"Liepaja" +LV,17,"Liepajas" +LV,18,"Limbazu" +LV,19,"Ludzas" +LV,20,"Madonas" +LV,21,"Ogres" +LV,22,"Preilu" +LV,23,"Rezekne" +LV,24,"Rezeknes" +LV,25,"Riga" +LV,26,"Rigas" +LV,27,"Saldus" +LV,28,"Talsu" +LV,29,"Tukuma" +LV,30,"Valkas" +LV,31,"Valmieras" +LV,32,"Ventspils" +LV,33,"Ventspils" +LY,03,"Al Aziziyah" +LY,05,"Al Jufrah" +LY,08,"Al Kufrah" +LY,13,"Ash Shati'" +LY,30,"Murzuq" +LY,34,"Sabha" +LY,41,"Tarhunah" +LY,42,"Tubruq" +LY,45,"Zlitan" +LY,47,"Ajdabiya" +LY,48,"Al Fatih" +LY,49,"Al Jabal al Akhdar" +LY,50,"Al Khums" +LY,51,"An Nuqat al Khams" +LY,52,"Awbari" +LY,53,"Az Zawiyah" +LY,54,"Banghazi" +LY,55,"Darnah" +LY,56,"Ghadamis" +LY,57,"Gharyan" +LY,58,"Misratah" +LY,59,"Sawfajjin" +LY,60,"Surt" +LY,61,"Tarabulus" +LY,62,"Yafran" +MA,45,"Grand Casablanca" +MA,46,"Fes-Boulemane" +MA,47,"Marrakech-Tensift-Al Haouz" +MA,48,"Meknes-Tafilalet" +MA,49,"Rabat-Sale-Zemmour-Zaer" +MA,50,"Chaouia-Ouardigha" +MA,51,"Doukkala-Abda" +MA,52,"Gharb-Chrarda-Beni Hssen" +MA,53,"Guelmim-Es Smara" +MA,54,"Oriental" +MA,55,"Souss-Massa-Dr,a" +MA,56,"Tadla-Azilal" +MA,57,"Tanger-Tetouan" +MA,58,"Taza-Al Hoceima-Taounate" +MA,59,"La,youne-Boujdour-Sakia El Hamra" +MC,01,"La Condamine" +MC,02,"Monaco" +MC,03,"Monte-Carlo" +MD,51,"Gagauzia" +MD,57,"Chisinau" +MD,58,"Stinga Nistrului" +MD,59,"Anenii Noi" +MD,60,"Balti" +MD,61,"Basarabeasca" +MD,62,"Bender" +MD,63,"Briceni" +MD,64,"Cahul" +MD,65,"Cantemir" +MD,66,"Calarasi" +MD,67,"Causeni" +MD,68,"Cimislia" +MD,69,"Criuleni" +MD,70,"Donduseni" +MD,71,"Drochia" +MD,72,"Dubasari" +MD,73,"Edinet" +MD,74,"Falesti" +MD,75,"Floresti" +MD,76,"Glodeni" +MD,77,"Hincesti" +MD,78,"Ialoveni" +MD,79,"Leova" +MD,80,"Nisporeni" +MD,81,"Ocnita" +MD,82,"Orhei" +MD,83,"Rezina" +MD,84,"Riscani" +MD,85,"Singerei" +MD,86,"Soldanesti" +MD,87,"Soroca" +MD,88,"Stefan-Voda" +MD,89,"Straseni" +MD,90,"Taraclia" +MD,91,"Telenesti" +MD,92,"Ungheni" +MG,01,"Antsiranana" +MG,02,"Fianarantsoa" +MG,03,"Mahajanga" +MG,04,"Toamasina" +MG,05,"Antananarivo" +MG,06,"Toliara" +MK,01,"Aracinovo" +MK,02,"Bac" +MK,03,"Belcista" +MK,04,"Berovo" +MK,05,"Bistrica" +MK,06,"Bitola" +MK,07,"Blatec" +MK,08,"Bogdanci" +MK,09,"Bogomila" +MK,10,"Bogovinje" +MK,11,"Bosilovo" +MK,12,"Brvenica" +MK,13,"Cair" +MK,14,"Capari" +MK,15,"Caska" +MK,16,"Cegrane" +MK,17,"Centar" +MK,18,"Centar Zupa" +MK,19,"Cesinovo" +MK,20,"Cucer-Sandevo" +MK,21,"Debar" +MK,22,"Delcevo" +MK,23,"Delogozdi" +MK,24,"Demir Hisar" +MK,25,"Demir Kapija" +MK,26,"Dobrusevo" +MK,27,"Dolna Banjica" +MK,28,"Dolneni" +MK,29,"Dorce Petrov" +MK,30,"Drugovo" +MK,31,"Dzepciste" +MK,32,"Gazi Baba" +MK,33,"Gevgelija" +MK,34,"Gostivar" +MK,35,"Gradsko" +MK,36,"Ilinden" +MK,37,"Izvor" +MK,38,"Jegunovce" +MK,39,"Kamenjane" +MK,40,"Karbinci" +MK,41,"Karpos" +MK,42,"Kavadarci" +MK,43,"Kicevo" +MK,44,"Kisela Voda" +MK,45,"Klecevce" +MK,46,"Kocani" +MK,47,"Konce" +MK,48,"Kondovo" +MK,49,"Konopiste" +MK,50,"Kosel" +MK,51,"Kratovo" +MK,52,"Kriva Palanka" +MK,53,"Krivogastani" +MK,54,"Krusevo" +MK,55,"Kuklis" +MK,56,"Kukurecani" +MK,57,"Kumanovo" +MK,58,"Labunista" +MK,59,"Lipkovo" +MK,60,"Lozovo" +MK,61,"Lukovo" +MK,62,"Makedonska Kamenica" +MK,63,"Makedonski Brod" +MK,64,"Mavrovi Anovi" +MK,65,"Meseista" +MK,66,"Miravci" +MK,67,"Mogila" +MK,68,"Murtino" +MK,69,"Negotino" +MK,70,"Negotino-Polosko" +MK,71,"Novaci" +MK,72,"Novo Selo" +MK,73,"Oblesevo" +MK,74,"Ohrid" +MK,75,"Orasac" +MK,76,"Orizari" +MK,77,"Oslomej" +MK,78,"Pehcevo" +MK,79,"Petrovec" +MK,80,"Plasnica" +MK,81,"Podares" +MK,82,"Prilep" +MK,83,"Probistip" +MK,84,"Radovis" +MK,85,"Rankovce" +MK,86,"Resen" +MK,87,"Rosoman" +MK,88,"Rostusa" +MK,89,"Samokov" +MK,90,"Saraj" +MK,91,"Sipkovica" +MK,92,"Sopiste" +MK,93,"Sopotnica" +MK,94,"Srbinovo" +MK,95,"Staravina" +MK,96,"Star Dojran" +MK,97,"Staro Nagoricane" +MK,98,"Stip" +MK,99,"Struga" +MK,A1,"Strumica" +MK,A2,"Studenicani" +MK,A3,"Suto Orizari" +MK,A4,"Sveti Nikole" +MK,A5,"Tearce" +MK,A6,"Tetovo" +MK,A7,"Topolcani" +MK,A8,"Valandovo" +MK,A9,"Vasilevo" +MK,B1,"Veles" +MK,B2,"Velesta" +MK,B3,"Vevcani" +MK,B4,"Vinica" +MK,B5,"Vitoliste" +MK,B6,"Vranestica" +MK,B7,"Vrapciste" +MK,B8,"Vratnica" +MK,B9,"Vrutok" +MK,C1,"Zajas" +MK,C2,"Zelenikovo" +MK,C3,"Zelino" +MK,C4,"Zitose" +MK,C5,"Zletovo" +MK,C6,"Zrnovci" +MK,C8,"Cair" +MK,C9,"Caska" +MK,D2,"Debar" +MK,D3,"Demir Hisar" +MK,D4,"Gostivar" +MK,D6,"Kavadarci" +MK,D7,"Kumanovo" +MK,D8,"Makedonski Brod" +MK,E2,"Ohrid" +MK,E3,"Prilep" +MK,E5,"Dojran" +MK,E6,"Struga" +MK,E7,"Strumica" +MK,E8,"Tetovo" +MK,E9,"Valandovo" +MK,F1,"Veles" +MK,F2,"Aerodrom" +ML,01,"Bamako" +ML,03,"Kayes" +ML,04,"Mopti" +ML,05,"Segou" +ML,06,"Sikasso" +ML,07,"Koulikoro" +ML,08,"Tombouctou" +ML,09,"Gao" +ML,10,"Kidal" +MM,01,"Rakhine State" +MM,02,"Chin State" +MM,03,"Irrawaddy" +MM,04,"Kachin State" +MM,05,"Karan State" +MM,06,"Kayah State" +MM,07,"Magwe" +MM,08,"Mandalay" +MM,09,"Pegu" +MM,10,"Sagaing" +MM,11,"Shan State" +MM,12,"Tenasserim" +MM,13,"Mon State" +MM,14,"Rangoon" +MM,17,"Yangon" +MN,01,"Arhangay" +MN,02,"Bayanhongor" +MN,03,"Bayan-Olgiy" +MN,05,"Darhan" +MN,06,"Dornod" +MN,07,"Dornogovi" +MN,08,"Dundgovi" +MN,09,"Dzavhan" +MN,10,"Govi-Altay" +MN,11,"Hentiy" +MN,12,"Hovd" +MN,13,"Hovsgol" +MN,14,"Omnogovi" +MN,15,"Ovorhangay" +MN,16,"Selenge" +MN,17,"Suhbaatar" +MN,18,"Tov" +MN,19,"Uvs" +MN,20,"Ulaanbaatar" +MN,21,"Bulgan" +MN,22,"Erdenet" +MN,23,"Darhan-Uul" +MN,24,"Govisumber" +MN,25,"Orhon" +MO,01,"Ilhas" +MO,02,"Macau" +MR,01,"Hodh Ech Chargui" +MR,02,"Hodh El Gharbi" +MR,03,"Assaba" +MR,04,"Gorgol" +MR,05,"Brakna" +MR,06,"Trarza" +MR,07,"Adrar" +MR,08,"Dakhlet Nouadhibou" +MR,09,"Tagant" +MR,10,"Guidimaka" +MR,11,"Tiris Zemmour" +MR,12,"Inchiri" +MS,01,"Saint Anthony" +MS,02,"Saint Georges" +MS,03,"Saint Peter" +MU,12,"Black River" +MU,13,"Flacq" +MU,14,"Grand Port" +MU,15,"Moka" +MU,16,"Pamplemousses" +MU,17,"Plaines Wilhems" +MU,18,"Port Louis" +MU,19,"Riviere du Rempart" +MU,20,"Savanne" +MU,21,"Agalega Islands" +MU,22,"Cargados Carajos" +MU,23,"Rodrigues" +MV,01,"Seenu" +MV,05,"Laamu" +MV,30,"Alifu" +MV,31,"Baa" +MV,32,"Dhaalu" +MV,33,"Faafu " +MV,34,"Gaafu Alifu" +MV,35,"Gaafu Dhaalu" +MV,36,"Haa Alifu" +MV,37,"Haa Dhaalu" +MV,38,"Kaafu" +MV,39,"Lhaviyani" +MV,40,"Maale" +MV,41,"Meemu" +MV,42,"Gnaviyani" +MV,43,"Noonu" +MV,44,"Raa" +MV,45,"Shaviyani" +MV,46,"Thaa" +MV,47,"Vaavu" +MW,02,"Chikwawa" +MW,03,"Chiradzulu" +MW,04,"Chitipa" +MW,05,"Thyolo" +MW,06,"Dedza" +MW,07,"Dowa" +MW,08,"Karonga" +MW,09,"Kasungu" +MW,11,"Lilongwe" +MW,12,"Mangochi" +MW,13,"Mchinji" +MW,15,"Mzimba" +MW,16,"Ntcheu" +MW,17,"Nkhata Bay" +MW,18,"Nkhotakota" +MW,19,"Nsanje" +MW,20,"Ntchisi" +MW,21,"Rumphi" +MW,22,"Salima" +MW,23,"Zomba" +MW,24,"Blantyre" +MW,25,"Mwanza" +MW,26,"Balaka" +MW,27,"Likoma" +MW,28,"Machinga" +MW,29,"Mulanje" +MW,30,"Phalombe" +MX,01,"Aguascalientes" +MX,02,"Baja California" +MX,03,"Baja California Sur" +MX,04,"Campeche" +MX,05,"Chiapas" +MX,06,"Chihuahua" +MX,07,"Coahuila de Zaragoza" +MX,08,"Colima" +MX,09,"Distrito Federal" +MX,10,"Durango" +MX,11,"Guanajuato" +MX,12,"Guerrero" +MX,13,"Hidalgo" +MX,14,"Jalisco" +MX,15,"Mexico" +MX,16,"Michoacan de Ocampo" +MX,17,"Morelos" +MX,18,"Nayarit" +MX,19,"Nuevo Leon" +MX,20,"Oaxaca" +MX,21,"Puebla" +MX,22,"Queretaro de Arteaga" +MX,23,"Quintana Roo" +MX,24,"San Luis Potosi" +MX,25,"Sinaloa" +MX,26,"Sonora" +MX,27,"Tabasco" +MX,28,"Tamaulipas" +MX,29,"Tlaxcala" +MX,30,"Veracruz-Llave" +MX,31,"Yucatan" +MX,32,"Zacatecas" +MY,01,"Johor" +MY,02,"Kedah" +MY,03,"Kelantan" +MY,04,"Melaka" +MY,05,"Negeri Sembilan" +MY,06,"Pahang" +MY,07,"Perak" +MY,08,"Perlis" +MY,09,"Pulau Pinang" +MY,11,"Sarawak" +MY,12,"Selangor" +MY,13,"Terengganu" +MY,14,"Kuala Lumpur" +MY,15,"Labuan" +MY,16,"Sabah" +MY,17,"Putrajaya" +MZ,01,"Cabo Delgado" +MZ,02,"Gaza" +MZ,03,"Inhambane" +MZ,04,"Maputo" +MZ,05,"Sofala" +MZ,06,"Nampula" +MZ,07,"Niassa" +MZ,08,"Tete" +MZ,09,"Zambezia" +MZ,10,"Manica" +MZ,11,"Maputo" +NA,01,"Bethanien" +NA,02,"Caprivi Oos" +NA,03,"Boesmanland" +NA,04,"Gobabis" +NA,05,"Grootfontein" +NA,06,"Kaokoland" +NA,07,"Karibib" +NA,08,"Keetmanshoop" +NA,09,"Luderitz" +NA,10,"Maltahohe" +NA,11,"Okahandja" +NA,12,"Omaruru" +NA,13,"Otjiwarongo" +NA,14,"Outjo" +NA,15,"Owambo" +NA,16,"Rehoboth" +NA,17,"Swakopmund" +NA,18,"Tsumeb" +NA,20,"Karasburg" +NA,21,"Windhoek" +NA,22,"Damaraland" +NA,23,"Hereroland Oos" +NA,24,"Hereroland Wes" +NA,25,"Kavango" +NA,26,"Mariental" +NA,27,"Namaland" +NA,28,"Caprivi" +NA,29,"Erongo" +NA,30,"Hardap" +NA,31,"Karas" +NA,32,"Kunene" +NA,33,"Ohangwena" +NA,34,"Okavango" +NA,35,"Omaheke" +NA,36,"Omusati" +NA,37,"Oshana" +NA,38,"Oshikoto" +NA,39,"Otjozondjupa" +NE,01,"Agadez" +NE,02,"Diffa" +NE,03,"Dosso" +NE,04,"Maradi" +NE,05,"Niamey" +NE,06,"Tahoua" +NE,07,"Zinder" +NE,08,"Niamey" +NG,05,"Lagos" +NG,11,"Federal Capital Territory" +NG,16,"Ogun" +NG,21,"Akwa Ibom" +NG,22,"Cross River" +NG,23,"Kaduna" +NG,24,"Katsina" +NG,25,"Anambra" +NG,26,"Benue" +NG,27,"Borno" +NG,28,"Imo" +NG,29,"Kano" +NG,30,"Kwara" +NG,31,"Niger" +NG,32,"Oyo" +NG,35,"Adamawa" +NG,36,"Delta" +NG,37,"Edo" +NG,39,"Jigawa" +NG,40,"Kebbi" +NG,41,"Kogi" +NG,42,"Osun" +NG,43,"Taraba" +NG,44,"Yobe" +NG,45,"Abia" +NG,46,"Bauchi" +NG,47,"Enugu" +NG,48,"Ondo" +NG,49,"Plateau" +NG,50,"Rivers" +NG,51,"Sokoto" +NG,52,"Bayelsa" +NG,53,"Ebonyi" +NG,54,"Ekiti" +NG,55,"Gombe" +NG,56,"Nassarawa" +NG,57,"Zamfara" +NI,01,"Boaco" +NI,02,"Carazo" +NI,03,"Chinandega" +NI,04,"Chontales" +NI,05,"Esteli" +NI,06,"Granada" +NI,07,"Jinotega" +NI,08,"Leon" +NI,09,"Madriz" +NI,10,"Managua" +NI,11,"Masaya" +NI,12,"Matagalpa" +NI,13,"Nueva Segovia" +NI,14,"Rio San Juan" +NI,15,"Rivas" +NI,16,"Zelaya" +NI,17,"Autonoma Atlantico Norte" +NI,18,"Region Autonoma Atlantico Sur" +NL,01,"Drenthe" +NL,02,"Friesland" +NL,03,"Gelderland" +NL,04,"Groningen" +NL,05,"Limburg" +NL,06,"Noord-Brabant" +NL,07,"Noord-Holland" +NL,09,"Utrecht" +NL,10,"Zeeland" +NL,11,"Zuid-Holland" +NL,15,"Overijssel" +NL,16,"Flevoland" +NO,01,"Akershus" +NO,02,"Aust-Agder" +NO,04,"Buskerud" +NO,05,"Finnmark" +NO,06,"Hedmark" +NO,07,"Hordaland" +NO,08,"More og Romsdal" +NO,09,"Nordland" +NO,10,"Nord-Trondelag" +NO,11,"Oppland" +NO,12,"Oslo" +NO,13,"Ostfold" +NO,14,"Rogaland" +NO,15,"Sogn og Fjordane" +NO,16,"Sor-Trondelag" +NO,17,"Telemark" +NO,18,"Troms" +NO,19,"Vest-Agder" +NO,20,"Vestfold" +NP,01,"Bagmati" +NP,02,"Bheri" +NP,03,"Dhawalagiri" +NP,04,"Gandaki" +NP,05,"Janakpur" +NP,06,"Karnali" +NP,07,"Kosi" +NP,08,"Lumbini" +NP,09,"Mahakali" +NP,10,"Mechi" +NP,11,"Narayani" +NP,12,"Rapti" +NP,13,"Sagarmatha" +NP,14,"Seti" +NR,01,"Aiwo" +NR,02,"Anabar" +NR,03,"Anetan" +NR,04,"Anibare" +NR,05,"Baiti" +NR,06,"Boe" +NR,07,"Buada" +NR,08,"Denigomodu" +NR,09,"Ewa" +NR,10,"Ijuw" +NR,11,"Meneng" +NR,12,"Nibok" +NR,13,"Uaboe" +NR,14,"Yaren" +NZ,10,"Chatham Islands" +NZ,E7,"Auckland" +NZ,E8,"Bay of Plenty" +NZ,E9,"Canterbury" +NZ,F1,"Gisborne" +NZ,F2,"Hawke's Bay" +NZ,F3,"Manawatu-Wanganui" +NZ,F4,"Marlborough" +NZ,F5,"Nelson" +NZ,F6,"Northland" +NZ,F7,"Otago" +NZ,F8,"Southland" +NZ,F9,"Taranaki" +NZ,G1,"Waikato" +NZ,G2,"Wellington" +NZ,G3,"West Coast" +OM,01,"Ad Dakhiliyah" +OM,02,"Al Batinah" +OM,03,"Al Wusta" +OM,04,"Ash Sharqiyah" +OM,05,"Az Zahirah" +OM,06,"Masqat" +OM,07,"Musandam" +OM,08,"Zufar" +PA,01,"Bocas del Toro" +PA,02,"Chiriqui" +PA,03,"Cocle" +PA,04,"Colon" +PA,05,"Darien" +PA,06,"Herrera" +PA,07,"Los Santos" +PA,08,"Panama" +PA,09,"San Blas" +PA,10,"Veraguas" +PE,01,"Amazonas" +PE,02,"Ancash" +PE,03,"Apurimac" +PE,04,"Arequipa" +PE,05,"Ayacucho" +PE,06,"Cajamarca" +PE,07,"Callao" +PE,08,"Cusco" +PE,09,"Huancavelica" +PE,10,"Huanuco" +PE,11,"Ica" +PE,12,"Junin" +PE,13,"La Libertad" +PE,14,"Lambayeque" +PE,15,"Lima" +PE,16,"Loreto" +PE,17,"Madre de Dios" +PE,18,"Moquegua" +PE,19,"Pasco" +PE,20,"Piura" +PE,21,"Puno" +PE,22,"San Martin" +PE,23,"Tacna" +PE,24,"Tumbes" +PE,25,"Ucayali" +PG,01,"Central" +PG,02,"Gulf" +PG,03,"Milne Bay" +PG,04,"Northern" +PG,05,"Southern Highlands" +PG,06,"Western" +PG,07,"North Solomons" +PG,08,"Chimbu" +PG,09,"Eastern Highlands" +PG,10,"East New Britain" +PG,11,"East Sepik" +PG,12,"Madang" +PG,13,"Manus" +PG,14,"Morobe" +PG,15,"New Ireland" +PG,16,"Western Highlands" +PG,17,"West New Britain" +PG,18,"Sandaun" +PG,19,"Enga" +PG,20,"National Capital" +PH,01,"Abra" +PH,02,"Agusan del Norte" +PH,03,"Agusan del Sur" +PH,04,"Aklan" +PH,05,"Albay" +PH,06,"Antique" +PH,07,"Bataan" +PH,08,"Batanes" +PH,09,"Batangas" +PH,10,"Benguet" +PH,11,"Bohol" +PH,12,"Bukidnon" +PH,13,"Bulacan" +PH,14,"Cagayan" +PH,15,"Camarines Norte" +PH,16,"Camarines Sur" +PH,17,"Camiguin" +PH,18,"Capiz" +PH,19,"Catanduanes" +PH,20,"Cavite" +PH,21,"Cebu" +PH,22,"Basilan" +PH,23,"Eastern Samar" +PH,24,"Davao" +PH,25,"Davao del Sur" +PH,26,"Davao Oriental" +PH,27,"Ifugao" +PH,28,"Ilocos Norte" +PH,29,"Ilocos Sur" +PH,30,"Iloilo" +PH,31,"Isabela" +PH,32,"Kalinga-Apayao" +PH,33,"Laguna" +PH,34,"Lanao del Norte" +PH,35,"Lanao del Sur" +PH,36,"La Union" +PH,37,"Leyte" +PH,38,"Marinduque" +PH,39,"Masbate" +PH,40,"Mindoro Occidental" +PH,41,"Mindoro Oriental" +PH,42,"Misamis Occidental" +PH,43,"Misamis Oriental" +PH,44,"Mountain" +PH,45,"Negros Occidental" +PH,46,"Negros Oriental" +PH,47,"Nueva Ecija" +PH,48,"Nueva Vizcaya" +PH,49,"Palawan" +PH,50,"Pampanga" +PH,51,"Pangasinan" +PH,53,"Rizal" +PH,54,"Romblon" +PH,55,"Samar" +PH,56,"Maguindanao" +PH,57,"North Cotabato" +PH,58,"Sorsogon" +PH,59,"Southern Leyte" +PH,60,"Sulu" +PH,61,"Surigao del Norte" +PH,62,"Surigao del Sur" +PH,63,"Tarlac" +PH,64,"Zambales" +PH,65,"Zamboanga del Norte" +PH,66,"Zamboanga del Sur" +PH,67,"Northern Samar" +PH,68,"Quirino" +PH,69,"Siquijor" +PH,70,"South Cotabato" +PH,71,"Sultan Kudarat" +PH,72,"Tawitawi" +PH,A1,"Angeles" +PH,A2,"Bacolod" +PH,A3,"Bago" +PH,A4,"Baguio" +PH,A5,"Bais" +PH,A6,"Basilan City" +PH,A7,"Batangas City" +PH,A8,"Butuan" +PH,A9,"Cabanatuan" +PH,B1,"Cadiz" +PH,B2,"Cagayan de Oro" +PH,B3,"Calbayog" +PH,B4,"Caloocan" +PH,B5,"Canlaon" +PH,B6,"Cavite City" +PH,B7,"Cebu City" +PH,B8,"Cotabato" +PH,B9,"Dagupan" +PH,C1,"Danao" +PH,C2,"Dapitan" +PH,C3,"Davao City" +PH,C4,"Dipolog" +PH,C5,"Dumaguete" +PH,C6,"General Santos" +PH,C7,"Gingoog" +PH,C8,"Iligan" +PH,C9,"Iloilo City" +PH,D1,"Iriga" +PH,D2,"La Carlota" +PH,D3,"Laoag" +PH,D4,"Lapu-Lapu" +PH,D5,"Legaspi" +PH,D6,"Lipa" +PH,D7,"Lucena" +PH,D8,"Mandaue" +PH,D9,"Manila" +PH,E1,"Marawi" +PH,E2,"Naga" +PH,E3,"Olongapo" +PH,E4,"Ormoc" +PH,E5,"Oroquieta" +PH,E6,"Ozamis" +PH,E7,"Pagadian" +PH,E8,"Palayan" +PH,E9,"Pasay" +PH,F1,"Puerto Princesa" +PH,F2,"Quezon City" +PH,F3,"Roxas" +PH,F4,"San Carlos" +PH,F5,"San Carlos" +PH,F6,"San Jose" +PH,F7,"San Pablo" +PH,F8,"Silay" +PH,F9,"Surigao" +PH,G1,"Tacloban" +PH,G2,"Tagaytay" +PH,G3,"Tagbilaran" +PH,G4,"Tangub" +PH,G5,"Toledo" +PH,G6,"Trece Martires" +PH,G7,"Zamboanga" +PH,G8,"Aurora" +PH,H2,"Quezon" +PH,H3,"Negros Occidental" +PH,H9,"Biliran" +PH,I6,"Compostela Valley" +PH,I7,"Davao del Norte" +PH,J3,"Guimaras" +PH,J4,"Himamaylan" +PH,J7,"Kalinga" +PH,K1,"Las Pinas" +PH,K5,"Malabon" +PH,K6,"Malaybalay" +PH,L4,"Muntinlupa" +PH,L5,"Navotas" +PH,L7,"Paranaque" +PH,L9,"Passi" +PH,P1,"Zambales" +PH,M5,"San Jose del Monte" +PH,M6,"San Juan" +PH,M8,"Santiago" +PH,M9,"Sarangani" +PH,N1,"Sipalay" +PH,N3,"Surigao del Norte" +PH,P2,"Zamboanga" +PK,01,"Federally Administered Tribal Areas" +PK,02,"Balochistan" +PK,03,"North-West Frontier" +PK,04,"Punjab" +PK,05,"Sindh" +PK,06,"Azad Kashmir" +PK,07,"Northern Areas" +PK,08,"Islamabad" +PL,72,"Dolnoslaskie" +PL,73,"Kujawsko-Pomorskie" +PL,74,"Lodzkie" +PL,75,"Lubelskie" +PL,76,"Lubuskie" +PL,77,"Malopolskie" +PL,78,"Mazowieckie" +PL,79,"Opolskie" +PL,80,"Podkarpackie" +PL,81,"Podlaskie" +PL,82,"Pomorskie" +PL,83,"Slaskie" +PL,84,"Swietokrzyskie" +PL,85,"Warminsko-Mazurskie" +PL,86,"Wielkopolskie" +PL,87,"Zachodniopomorskie" +PS,GZ,"Gaza" +PS,WE,"West Bank" +PT,02,"Aveiro" +PT,03,"Beja" +PT,04,"Braga" +PT,05,"Braganca" +PT,06,"Castelo Branco" +PT,07,"Coimbra" +PT,08,"Evora" +PT,09,"Faro" +PT,10,"Madeira" +PT,11,"Guarda" +PT,13,"Leiria" +PT,14,"Lisboa" +PT,16,"Portalegre" +PT,17,"Porto" +PT,18,"Santarem" +PT,19,"Setubal" +PT,20,"Viana do Castelo" +PT,21,"Vila Real" +PT,22,"Viseu" +PT,23,"Azores" +PY,01,"Alto Parana" +PY,02,"Amambay" +PY,04,"Caaguazu" +PY,05,"Caazapa" +PY,06,"Central" +PY,07,"Concepcion" +PY,08,"Cordillera" +PY,10,"Guaira" +PY,11,"Itapua" +PY,12,"Misiones" +PY,13,"Neembucu" +PY,15,"Paraguari" +PY,16,"Presidente Hayes" +PY,17,"San Pedro" +PY,19,"Canindeyu" +PY,22,"Asuncion" +PY,23,"Alto Paraguay" +PY,24,"Boqueron" +QA,01,"Ad Dawhah" +QA,02,"Al Ghuwariyah" +QA,03,"Al Jumaliyah" +QA,04,"Al Khawr" +QA,05,"Al Wakrah Municipality" +QA,06,"Ar Rayyan" +QA,08,"Madinat ach Shamal" +QA,09,"Umm Salal" +QA,10,"Al Wakrah" +QA,11,"Jariyan al Batnah" +QA,12,"Umm Sa'id" +RO,01,"Alba" +RO,02,"Arad" +RO,03,"Arges" +RO,04,"Bacau" +RO,05,"Bihor" +RO,06,"Bistrita-Nasaud" +RO,07,"Botosani" +RO,08,"Braila" +RO,09,"Brasov" +RO,10,"Bucuresti" +RO,11,"Buzau" +RO,12,"Caras-Severin" +RO,13,"Cluj" +RO,14,"Constanta" +RO,15,"Covasna" +RO,16,"Dambovita" +RO,17,"Dolj" +RO,18,"Galati" +RO,19,"Gorj" +RO,20,"Harghita" +RO,21,"Hunedoara" +RO,22,"Ialomita" +RO,23,"Iasi" +RO,25,"Maramures" +RO,26,"Mehedinti" +RO,27,"Mures" +RO,28,"Neamt" +RO,29,"Olt" +RO,30,"Prahova" +RO,31,"Salaj" +RO,32,"Satu Mare" +RO,33,"Sibiu" +RO,34,"Suceava" +RO,35,"Teleorman" +RO,36,"Timis" +RO,37,"Tulcea" +RO,38,"Vaslui" +RO,39,"Valcea" +RO,40,"Vrancea" +RO,41,"Calarasi" +RO,42,"Giurgiu" +RO,43,"Ilfov" +RS,01,"Kosovo" +RS,02,"Vojvodina" +RU,01,"Adygeya, Republic of" +RU,02,"Aginsky Buryatsky AO" +RU,03,"Gorno-Altay" +RU,04,"Altaisky krai" +RU,05,"Amur" +RU,06,"Arkhangel'sk" +RU,07,"Astrakhan'" +RU,08,"Bashkortostan" +RU,09,"Belgorod" +RU,10,"Bryansk" +RU,11,"Buryat" +RU,12,"Chechnya" +RU,13,"Chelyabinsk" +RU,14,"Chita" +RU,15,"Chukot" +RU,16,"Chuvashia" +RU,17,"Dagestan" +RU,18,"Evenk" +RU,19,"Ingush" +RU,20,"Irkutsk" +RU,21,"Ivanovo" +RU,22,"Kabardin-Balkar" +RU,23,"Kaliningrad" +RU,24,"Kalmyk" +RU,25,"Kaluga" +RU,26,"Kamchatka" +RU,27,"Karachay-Cherkess" +RU,28,"Karelia" +RU,29,"Kemerovo" +RU,30,"Khabarovsk" +RU,31,"Khakass" +RU,32,"Khanty-Mansiy" +RU,33,"Kirov" +RU,34,"Komi" +RU,36,"Koryak" +RU,37,"Kostroma" +RU,38,"Krasnodar" +RU,39,"Krasnoyarsk" +RU,40,"Kurgan" +RU,41,"Kursk" +RU,42,"Leningrad" +RU,43,"Lipetsk" +RU,44,"Magadan" +RU,45,"Mariy-El" +RU,46,"Mordovia" +RU,47,"Moskva" +RU,48,"Moscow City" +RU,49,"Murmansk" +RU,50,"Nenets" +RU,51,"Nizhegorod" +RU,52,"Novgorod" +RU,53,"Novosibirsk" +RU,54,"Omsk" +RU,55,"Orenburg" +RU,56,"Orel" +RU,57,"Penza" +RU,58,"Perm'" +RU,59,"Primor'ye" +RU,60,"Pskov" +RU,61,"Rostov" +RU,62,"Ryazan'" +RU,63,"Sakha" +RU,64,"Sakhalin" +RU,65,"Samara" +RU,66,"Saint Petersburg City" +RU,67,"Saratov" +RU,68,"North Ossetia" +RU,69,"Smolensk" +RU,70,"Stavropol'" +RU,71,"Sverdlovsk" +RU,72,"Tambovskaya oblast" +RU,73,"Tatarstan" +RU,74,"Taymyr" +RU,75,"Tomsk" +RU,76,"Tula" +RU,77,"Tver'" +RU,78,"Tyumen'" +RU,79,"Tuva" +RU,80,"Udmurt" +RU,81,"Ul'yanovsk" +RU,83,"Vladimir" +RU,84,"Volgograd" +RU,85,"Vologda" +RU,86,"Voronezh" +RU,87,"Yamal-Nenets" +RU,88,"Yaroslavl'" +RU,89,"Yevrey" +RU,90,"Permskiy Kray" +RU,91,"Krasnoyarskiy Kray" +RU,92,"Kamchatskiy Kray" +RU,93,"Zabaykal'skiy Kray" +RW,01,"Butare" +RW,06,"Gitarama" +RW,07,"Kibungo" +RW,09,"Kigali" +RW,11,"Est" +RW,12,"Kigali" +RW,13,"Nord" +RW,14,"Ouest" +RW,15,"Sud" +SA,02,"Al Bahah" +SA,05,"Al Madinah" +SA,06,"Ash Sharqiyah" +SA,08,"Al Qasim" +SA,10,"Ar Riyad" +SA,11,"Asir Province" +SA,13,"Ha'il" +SA,14,"Makkah" +SA,15,"Al Hudud ash Shamaliyah" +SA,16,"Najran" +SA,17,"Jizan" +SA,19,"Tabuk" +SA,20,"Al Jawf" +SB,03,"Malaita" +SB,06,"Guadalcanal" +SB,07,"Isabel" +SB,08,"Makira" +SB,09,"Temotu" +SB,10,"Central" +SB,11,"Western" +SB,12,"Choiseul" +SB,13,"Rennell and Bellona" +SC,01,"Anse aux Pins" +SC,02,"Anse Boileau" +SC,03,"Anse Etoile" +SC,04,"Anse Louis" +SC,05,"Anse Royale" +SC,06,"Baie Lazare" +SC,07,"Baie Sainte Anne" +SC,08,"Beau Vallon" +SC,09,"Bel Air" +SC,10,"Bel Ombre" +SC,11,"Cascade" +SC,12,"Glacis" +SC,13,"Grand' Anse" +SC,14,"Grand' Anse" +SC,15,"La Digue" +SC,16,"La Riviere Anglaise" +SC,17,"Mont Buxton" +SC,18,"Mont Fleuri" +SC,19,"Plaisance" +SC,20,"Pointe La Rue" +SC,21,"Port Glaud" +SC,22,"Saint Louis" +SC,23,"Takamaka" +SD,27,"Al Wusta" +SD,28,"Al Istiwa'iyah" +SD,29,"Al Khartum" +SD,30,"Ash Shamaliyah" +SD,31,"Ash Sharqiyah" +SD,32,"Bahr al Ghazal" +SD,33,"Darfur" +SD,34,"Kurdufan" +SD,35,"Upper Nile" +SD,40,"Al Wahadah State" +SD,44,"Central Equatoria State" +SD,49,"Southern Darfur" +SD,50,"Southern Kordofan" +SD,52,"Kassala" +SD,53,"River Nile" +SD,55,"Northern Darfur" +SE,02,"Blekinge Lan" +SE,03,"Gavleborgs Lan" +SE,05,"Gotlands Lan" +SE,06,"Hallands Lan" +SE,07,"Jamtlands Lan" +SE,08,"Jonkopings Lan" +SE,09,"Kalmar Lan" +SE,10,"Dalarnas Lan" +SE,12,"Kronobergs Lan" +SE,14,"Norrbottens Lan" +SE,15,"Orebro Lan" +SE,16,"Ostergotlands Lan" +SE,18,"Sodermanlands Lan" +SE,21,"Uppsala Lan" +SE,22,"Varmlands Lan" +SE,23,"Vasterbottens Lan" +SE,24,"Vasternorrlands Lan" +SE,25,"Vastmanlands Lan" +SE,26,"Stockholms Lan" +SE,27,"Skane Lan" +SE,28,"Vastra Gotaland" +SH,01,"Ascension" +SH,02,"Saint Helena" +SH,03,"Tristan da Cunha" +SI,01,"Ajdovscina Commune" +SI,02,"Beltinci Commune" +SI,03,"Bled Commune" +SI,04,"Bohinj Commune" +SI,05,"Borovnica Commune" +SI,06,"Bovec Commune" +SI,07,"Brda Commune" +SI,08,"Brezice Commune" +SI,09,"Brezovica Commune" +SI,11,"Celje Commune" +SI,12,"Cerklje na Gorenjskem Commune" +SI,13,"Cerknica Commune" +SI,14,"Cerkno Commune" +SI,15,"Crensovci Commune" +SI,16,"Crna na Koroskem Commune" +SI,17,"Crnomelj Commune" +SI,19,"Divaca Commune" +SI,20,"Dobrepolje Commune" +SI,22,"Dol pri Ljubljani Commune" +SI,24,"Dornava Commune" +SI,25,"Dravograd Commune" +SI,26,"Duplek Commune" +SI,27,"Gorenja vas-Poljane Commune" +SI,28,"Gorisnica Commune" +SI,29,"Gornja Radgona Commune" +SI,30,"Gornji Grad Commune" +SI,31,"Gornji Petrovci Commune" +SI,32,"Grosuplje Commune" +SI,34,"Hrastnik Commune" +SI,35,"Hrpelje-Kozina Commune" +SI,36,"Idrija Commune" +SI,37,"Ig Commune" +SI,38,"Ilirska Bistrica Commune" +SI,39,"Ivancna Gorica Commune" +SI,40,"Izola-Isola Commune" +SI,42,"Jursinci Commune" +SI,44,"Kanal Commune" +SI,45,"Kidricevo Commune" +SI,46,"Kobarid Commune" +SI,47,"Kobilje Commune" +SI,49,"Komen Commune" +SI,50,"Koper-Capodistria Urban Commune" +SI,51,"Kozje Commune" +SI,52,"Kranj Commune" +SI,53,"Kranjska Gora Commune" +SI,54,"Krsko Commune" +SI,55,"Kungota Commune" +SI,57,"Lasko Commune" +SI,61,"Ljubljana Urban Commune" +SI,62,"Ljubno Commune" +SI,64,"Logatec Commune" +SI,66,"Loski Potok Commune" +SI,68,"Lukovica Commune" +SI,71,"Medvode Commune" +SI,72,"Menges Commune" +SI,73,"Metlika Commune" +SI,74,"Mezica Commune" +SI,76,"Mislinja Commune" +SI,77,"Moravce Commune" +SI,78,"Moravske Toplice Commune" +SI,79,"Mozirje Commune" +SI,80,"Murska Sobota Urban Commune" +SI,81,"Muta Commune" +SI,82,"Naklo Commune" +SI,83,"Nazarje Commune" +SI,84,"Nova Gorica Urban Commune" +SI,86,"Odranci Commune" +SI,87,"Ormoz Commune" +SI,88,"Osilnica Commune" +SI,89,"Pesnica Commune" +SI,91,"Pivka Commune" +SI,92,"Podcetrtek Commune" +SI,94,"Postojna Commune" +SI,97,"Puconci Commune" +SI,98,"Race-Fram Commune" +SI,99,"Radece Commune" +SI,A1,"Radenci Commune" +SI,A2,"Radlje ob Dravi Commune" +SI,A3,"Radovljica Commune" +SI,A6,"Rogasovci Commune" +SI,A7,"Rogaska Slatina Commune" +SI,A8,"Rogatec Commune" +SI,B1,"Semic Commune" +SI,B2,"Sencur Commune" +SI,B3,"Sentilj Commune" +SI,B4,"Sentjernej Commune" +SI,B6,"Sevnica Commune" +SI,B7,"Sezana Commune" +SI,B8,"Skocjan Commune" +SI,B9,"Skofja Loka Commune" +SI,C1,"Skofljica Commune" +SI,C2,"Slovenj Gradec Urban Commune" +SI,C4,"Slovenske Konjice Commune" +SI,C5,"Smarje pri Jelsah Commune" +SI,C6,"Smartno ob Paki Commune" +SI,C7,"Sostanj Commune" +SI,C8,"Starse Commune" +SI,C9,"Store Commune" +SI,D1,"Sveti Jurij Commune" +SI,D2,"Tolmin Commune" +SI,D3,"Trbovlje Commune" +SI,D4,"Trebnje Commune" +SI,D5,"Trzic Commune" +SI,D6,"Turnisce Commune" +SI,D7,"Velenje Urban Commune" +SI,D8,"Velike Lasce Commune" +SI,E1,"Vipava Commune" +SI,E2,"Vitanje Commune" +SI,E3,"Vodice Commune" +SI,E5,"Vrhnika Commune" +SI,E6,"Vuzenica Commune" +SI,E7,"Zagorje ob Savi Commune" +SI,E9,"Zavrc Commune" +SI,F1,"Zelezniki Commune" +SI,F2,"Ziri Commune" +SI,F3,"Zrece Commune" +SI,F4,"Benedikt Commune" +SI,F5,"Bistrica ob Sotli Commune" +SI,F6,"Bloke Commune" +SI,F7,"Braslovce Commune" +SI,F8,"Cankova Commune" +SI,F9,"Cerkvenjak Commune" +SI,G1,"Destrnik Commune" +SI,G2,"Dobje Commune" +SI,G3,"Dobrna Commune" +SI,G4,"Dobrova-Horjul-Polhov Gradec Commune" +SI,G5,"Dobrovnik-Dobronak Commune" +SI,G6,"Dolenjske Toplice Commune" +SI,G7,"Domzale Commune" +SI,G8,"Grad Commune" +SI,G9,"Hajdina Commune" +SI,H1,"Hoce-Slivnica Commune" +SI,H2,"Hodos-Hodos Commune" +SI,H3,"Horjul Commune" +SI,H4,"Jesenice Commune" +SI,H5,"Jezersko Commune" +SI,H6,"Kamnik Commune" +SI,H7,"Kocevje Commune" +SI,H8,"Komenda Commune" +SI,H9,"Kostel Commune" +SI,I1,"Krizevci Commune" +SI,I2,"Kuzma Commune" +SI,I3,"Lenart Commune" +SI,I4,"Lendava-Lendva Commune" +SI,I5,"Litija Commune" +SI,I6,"Ljutomer Commune" +SI,I7,"Loska Dolina Commune" +SI,I8,"Lovrenc na Pohorju Commune" +SI,I9,"Luce Commune" +SI,J1,"Majsperk Commune" +SI,J2,"Maribor Commune" +SI,J3,"Markovci Commune" +SI,J4,"Miklavz na Dravskem polju Commune" +SI,J5,"Miren-Kostanjevica Commune" +SI,J6,"Mirna Pec Commune" +SI,J7,"Novo mesto Urban Commune" +SI,J8,"Oplotnica Commune" +SI,J9,"Piran-Pirano Commune" +SI,K1,"Podlehnik Commune" +SI,K2,"Podvelka Commune" +SI,K3,"Polzela Commune" +SI,K4,"Prebold Commune" +SI,K5,"Preddvor Commune" +SI,K6,"Prevalje Commune" +SI,K7,"Ptuj Urban Commune" +SI,K8,"Ravne na Koroskem Commune" +SI,K9,"Razkrizje Commune" +SI,L1,"Ribnica Commune" +SI,L2,"Ribnica na Pohorju Commune" +SI,L3,"Ruse Commune" +SI,L4,"Salovci Commune" +SI,L5,"Selnica ob Dravi Commune" +SI,L6,"Sempeter-Vrtojba Commune" +SI,L7,"Sentjur pri Celju Commune" +SI,L8,"Slovenska Bistrica Commune" +SI,L9,"Smartno pri Litiji Commune" +SI,M1,"Sodrazica Commune" +SI,M2,"Solcava Commune" +SI,M3,"Sveta Ana Commune" +SI,M4,"Sveti Andraz v Slovenskih goricah Commune" +SI,M5,"Tabor Commune" +SI,M6,"Tisina Commune" +SI,M7,"Trnovska vas Commune" +SI,M8,"Trzin Commune" +SI,M9,"Velika Polana Commune" +SI,N1,"Verzej Commune" +SI,N2,"Videm Commune" +SI,N3,"Vojnik Commune" +SI,N4,"Vransko Commune" +SI,N5,"Zalec Commune" +SI,N6,"Zetale Commune" +SI,N7,"Zirovnica Commune" +SI,N8,"Zuzemberk Commune" +SI,N9,"Apace Commune" +SI,O1,"Cirkulane Commune" +SI,O2,"Gorje" +SI,O3,"Kostanjevica na Krki" +SI,O4,"Log-Dragomer" +SI,O5,"Makole" +SI,O6,"Mirna" +SI,O7,"Mokronog-Trebelno" +SI,O8,"Poljcane" +SI,O9,"Recica ob Savinji" +SI,P1,"Rence-Vogrsko" +SI,P2,"Sentrupert" +SI,P3,"Smarjesk Toplice" +SI,P4,"Sredisce ob Dravi" +SI,P5,"Straza" +SI,P7,"Sveti Jurij v Slovenskih Goricah" +SK,01,"Banska Bystrica" +SK,02,"Bratislava" +SK,03,"Kosice" +SK,04,"Nitra" +SK,05,"Presov" +SK,06,"Trencin" +SK,07,"Trnava" +SK,08,"Zilina" +SL,01,"Eastern" +SL,02,"Northern" +SL,03,"Southern" +SL,04,"Western Area" +SM,01,"Acquaviva" +SM,02,"Chiesanuova" +SM,03,"Domagnano" +SM,04,"Faetano" +SM,05,"Fiorentino" +SM,06,"Borgo Maggiore" +SM,07,"San Marino" +SM,08,"Monte Giardino" +SM,09,"Serravalle" +SN,01,"Dakar" +SN,03,"Diourbel" +SN,05,"Tambacounda" +SN,07,"Thies" +SN,09,"Fatick" +SN,10,"Kaolack" +SN,11,"Kolda" +SN,12,"Ziguinchor" +SN,13,"Louga" +SN,14,"Saint-Louis" +SN,15,"Matam" +SO,01,"Bakool" +SO,02,"Banaadir" +SO,03,"Bari" +SO,04,"Bay" +SO,05,"Galguduud" +SO,06,"Gedo" +SO,07,"Hiiraan" +SO,08,"Jubbada Dhexe" +SO,09,"Jubbada Hoose" +SO,10,"Mudug" +SO,11,"Nugaal" +SO,12,"Sanaag" +SO,13,"Shabeellaha Dhexe" +SO,14,"Shabeellaha Hoose" +SO,16,"Woqooyi Galbeed" +SO,18,"Nugaal" +SO,19,"Togdheer" +SO,20,"Woqooyi Galbeed" +SO,21,"Awdal" +SO,22,"Sool" +SR,10,"Brokopondo" +SR,11,"Commewijne" +SR,12,"Coronie" +SR,13,"Marowijne" +SR,14,"Nickerie" +SR,15,"Para" +SR,16,"Paramaribo" +SR,17,"Saramacca" +SR,18,"Sipaliwini" +SR,19,"Wanica" +SS,01,"Central Equatoria" +SS,02,"Eastern Equatoria" +SS,03,"Jonglei" +SS,04,"Lakes" +SS,05,"Northern Bahr el Ghazal" +SS,06,"Unity" +SS,07,"Upper Nile" +SS,08,"Warrap" +SS,09,"Western Bahr el Ghazal" +SS,10,"Western Equatoria" +ST,01,"Principe" +ST,02,"Sao Tome" +SV,01,"Ahuachapan" +SV,02,"Cabanas" +SV,03,"Chalatenango" +SV,04,"Cuscatlan" +SV,05,"La Libertad" +SV,06,"La Paz" +SV,07,"La Union" +SV,08,"Morazan" +SV,09,"San Miguel" +SV,10,"San Salvador" +SV,11,"Santa Ana" +SV,12,"San Vicente" +SV,13,"Sonsonate" +SV,14,"Usulutan" +SY,01,"Al Hasakah" +SY,02,"Al Ladhiqiyah" +SY,03,"Al Qunaytirah" +SY,04,"Ar Raqqah" +SY,05,"As Suwayda'" +SY,06,"Dar" +SY,07,"Dayr az Zawr" +SY,08,"Rif Dimashq" +SY,09,"Halab" +SY,10,"Hamah" +SY,11,"Hims" +SY,12,"Idlib" +SY,13,"Dimashq" +SY,14,"Tartus" +SZ,01,"Hhohho" +SZ,02,"Lubombo" +SZ,03,"Manzini" +SZ,04,"Shiselweni" +SZ,05,"Praslin" +TD,01,"Batha" +TD,02,"Biltine" +TD,03,"Borkou-Ennedi-Tibesti" +TD,04,"Chari-Baguirmi" +TD,05,"Guera" +TD,06,"Kanem" +TD,07,"Lac" +TD,08,"Logone Occidental" +TD,09,"Logone Oriental" +TD,10,"Mayo-Kebbi" +TD,11,"Moyen-Chari" +TD,12,"Ouaddai" +TD,13,"Salamat" +TD,14,"Tandjile" +TG,22,"Centrale" +TG,23,"Kara" +TG,24,"Maritime" +TG,25,"Plateaux" +TG,26,"Savanes" +TH,01,"Mae Hong Son" +TH,02,"Chiang Mai" +TH,03,"Chiang Rai" +TH,04,"Nan" +TH,05,"Lamphun" +TH,06,"Lampang" +TH,07,"Phrae" +TH,08,"Tak" +TH,09,"Sukhothai" +TH,10,"Uttaradit" +TH,11,"Kamphaeng Phet" +TH,12,"Phitsanulok" +TH,13,"Phichit" +TH,14,"Phetchabun" +TH,15,"Uthai Thani" +TH,16,"Nakhon Sawan" +TH,17,"Nong Khai" +TH,18,"Loei" +TH,20,"Sakon Nakhon" +TH,21,"Nakhon Phanom" +TH,22,"Khon Kaen" +TH,23,"Kalasin" +TH,24,"Maha Sarakham" +TH,25,"Roi Et" +TH,26,"Chaiyaphum" +TH,27,"Nakhon Ratchasima" +TH,28,"Buriram" +TH,29,"Surin" +TH,30,"Sisaket" +TH,31,"Narathiwat" +TH,32,"Chai Nat" +TH,33,"Sing Buri" +TH,34,"Lop Buri" +TH,35,"Ang Thong" +TH,36,"Phra Nakhon Si Ayutthaya" +TH,37,"Saraburi" +TH,38,"Nonthaburi" +TH,39,"Pathum Thani" +TH,40,"Krung Thep" +TH,41,"Phayao" +TH,42,"Samut Prakan" +TH,43,"Nakhon Nayok" +TH,44,"Chachoengsao" +TH,45,"Prachin Buri" +TH,46,"Chon Buri" +TH,47,"Rayong" +TH,48,"Chanthaburi" +TH,49,"Trat" +TH,50,"Kanchanaburi" +TH,51,"Suphan Buri" +TH,52,"Ratchaburi" +TH,53,"Nakhon Pathom" +TH,54,"Samut Songkhram" +TH,55,"Samut Sakhon" +TH,56,"Phetchaburi" +TH,57,"Prachuap Khiri Khan" +TH,58,"Chumphon" +TH,59,"Ranong" +TH,60,"Surat Thani" +TH,61,"Phangnga" +TH,62,"Phuket" +TH,63,"Krabi" +TH,64,"Nakhon Si Thammarat" +TH,65,"Trang" +TH,66,"Phatthalung" +TH,67,"Satun" +TH,68,"Songkhla" +TH,69,"Pattani" +TH,70,"Yala" +TH,71,"Ubon Ratchathani" +TH,72,"Yasothon" +TH,73,"Nakhon Phanom" +TH,74,"Prachin Buri" +TH,75,"Ubon Ratchathani" +TH,76,"Udon Thani" +TH,77,"Amnat Charoen" +TH,78,"Mukdahan" +TH,79,"Nong Bua Lamphu" +TH,80,"Sa Kaeo" +TH,81,"Bueng Kan" +TJ,01,"Kuhistoni Badakhshon" +TJ,02,"Khatlon" +TJ,03,"Sughd" +TJ,04,"Dushanbe" +TJ,05,"Nohiyahoi Tobei Jumhuri" +TL,06,"Dili" +TM,01,"Ahal" +TM,02,"Balkan" +TM,03,"Dashoguz" +TM,04,"Lebap" +TM,05,"Mary" +TN,02,"Kasserine" +TN,03,"Kairouan" +TN,06,"Jendouba" +TN,10,"Qafsah" +TN,14,"El Kef" +TN,15,"Al Mahdia" +TN,16,"Al Munastir" +TN,17,"Bajah" +TN,18,"Bizerte" +TN,19,"Nabeul" +TN,22,"Siliana" +TN,23,"Sousse" +TN,27,"Ben Arous" +TN,28,"Madanin" +TN,29,"Gabes" +TN,31,"Kebili" +TN,32,"Sfax" +TN,33,"Sidi Bou Zid" +TN,34,"Tataouine" +TN,35,"Tozeur" +TN,36,"Tunis" +TN,37,"Zaghouan" +TN,38,"Aiana" +TN,39,"Manouba" +TO,01,"Ha" +TO,02,"Tongatapu" +TO,03,"Vava" +TR,02,"Adiyaman" +TR,03,"Afyonkarahisar" +TR,04,"Agri" +TR,05,"Amasya" +TR,07,"Antalya" +TR,08,"Artvin" +TR,09,"Aydin" +TR,10,"Balikesir" +TR,11,"Bilecik" +TR,12,"Bingol" +TR,13,"Bitlis" +TR,14,"Bolu" +TR,15,"Burdur" +TR,16,"Bursa" +TR,17,"Canakkale" +TR,19,"Corum" +TR,20,"Denizli" +TR,21,"Diyarbakir" +TR,22,"Edirne" +TR,23,"Elazig" +TR,24,"Erzincan" +TR,25,"Erzurum" +TR,26,"Eskisehir" +TR,28,"Giresun" +TR,31,"Hatay" +TR,32,"Mersin" +TR,33,"Isparta" +TR,34,"Istanbul" +TR,35,"Izmir" +TR,37,"Kastamonu" +TR,38,"Kayseri" +TR,39,"Kirklareli" +TR,40,"Kirsehir" +TR,41,"Kocaeli" +TR,43,"Kutahya" +TR,44,"Malatya" +TR,45,"Manisa" +TR,46,"Kahramanmaras" +TR,48,"Mugla" +TR,49,"Mus" +TR,50,"Nevsehir" +TR,52,"Ordu" +TR,53,"Rize" +TR,54,"Sakarya" +TR,55,"Samsun" +TR,57,"Sinop" +TR,58,"Sivas" +TR,59,"Tekirdag" +TR,60,"Tokat" +TR,61,"Trabzon" +TR,62,"Tunceli" +TR,63,"Sanliurfa" +TR,64,"Usak" +TR,65,"Van" +TR,66,"Yozgat" +TR,68,"Ankara" +TR,69,"Gumushane" +TR,70,"Hakkari" +TR,71,"Konya" +TR,72,"Mardin" +TR,73,"Nigde" +TR,74,"Siirt" +TR,75,"Aksaray" +TR,76,"Batman" +TR,77,"Bayburt" +TR,78,"Karaman" +TR,79,"Kirikkale" +TR,80,"Sirnak" +TR,81,"Adana" +TR,82,"Cankiri" +TR,83,"Gaziantep" +TR,84,"Kars" +TR,85,"Zonguldak" +TR,86,"Ardahan" +TR,87,"Bartin" +TR,88,"Igdir" +TR,89,"Karabuk" +TR,90,"Kilis" +TR,91,"Osmaniye" +TR,92,"Yalova" +TR,93,"Duzce" +TT,01,"Arima" +TT,02,"Caroni" +TT,03,"Mayaro" +TT,04,"Nariva" +TT,05,"Port-of-Spain" +TT,06,"Saint Andrew" +TT,07,"Saint David" +TT,08,"Saint George" +TT,09,"Saint Patrick" +TT,10,"San Fernando" +TT,11,"Tobago" +TT,12,"Victoria" +TW,01,"Fu-chien" +TW,02,"Kao-hsiung" +TW,03,"T'ai-pei" +TW,04,"T'ai-wan" +TZ,02,"Pwani" +TZ,03,"Dodoma" +TZ,04,"Iringa" +TZ,05,"Kigoma" +TZ,06,"Kilimanjaro" +TZ,07,"Lindi" +TZ,08,"Mara" +TZ,09,"Mbeya" +TZ,10,"Morogoro" +TZ,11,"Mtwara" +TZ,12,"Mwanza" +TZ,13,"Pemba North" +TZ,14,"Ruvuma" +TZ,15,"Shinyanga" +TZ,16,"Singida" +TZ,17,"Tabora" +TZ,18,"Tanga" +TZ,19,"Kagera" +TZ,20,"Pemba South" +TZ,21,"Zanzibar Central" +TZ,22,"Zanzibar North" +TZ,23,"Dar es Salaam" +TZ,24,"Rukwa" +TZ,25,"Zanzibar Urban" +TZ,26,"Arusha" +TZ,27,"Manyara" +UA,01,"Cherkas'ka Oblast'" +UA,02,"Chernihivs'ka Oblast'" +UA,03,"Chernivets'ka Oblast'" +UA,04,"Dnipropetrovs'ka Oblast'" +UA,05,"Donets'ka Oblast'" +UA,06,"Ivano-Frankivs'ka Oblast'" +UA,07,"Kharkivs'ka Oblast'" +UA,08,"Khersons'ka Oblast'" +UA,09,"Khmel'nyts'ka Oblast'" +UA,10,"Kirovohrads'ka Oblast'" +UA,11,"Krym" +UA,12,"Kyyiv" +UA,13,"Kyyivs'ka Oblast'" +UA,14,"Luhans'ka Oblast'" +UA,15,"L'vivs'ka Oblast'" +UA,16,"Mykolayivs'ka Oblast'" +UA,17,"Odes'ka Oblast'" +UA,18,"Poltavs'ka Oblast'" +UA,19,"Rivnens'ka Oblast'" +UA,20,"Sevastopol'" +UA,21,"Sums'ka Oblast'" +UA,22,"Ternopil's'ka Oblast'" +UA,23,"Vinnyts'ka Oblast'" +UA,24,"Volyns'ka Oblast'" +UA,25,"Zakarpats'ka Oblast'" +UA,26,"Zaporiz'ka Oblast'" +UA,27,"Zhytomyrs'ka Oblast'" +UG,26,"Apac" +UG,28,"Bundibugyo" +UG,29,"Bushenyi" +UG,30,"Gulu" +UG,31,"Hoima" +UG,33,"Jinja" +UG,36,"Kalangala" +UG,37,"Kampala" +UG,38,"Kamuli" +UG,39,"Kapchorwa" +UG,40,"Kasese" +UG,41,"Kibale" +UG,42,"Kiboga" +UG,43,"Kisoro" +UG,45,"Kotido" +UG,46,"Kumi" +UG,47,"Lira" +UG,50,"Masindi" +UG,52,"Mbarara" +UG,56,"Mubende" +UG,58,"Nebbi" +UG,59,"Ntungamo" +UG,60,"Pallisa" +UG,61,"Rakai" +UG,65,"Adjumani" +UG,66,"Bugiri" +UG,67,"Busia" +UG,69,"Katakwi" +UG,70,"Luwero" +UG,71,"Masaka" +UG,72,"Moyo" +UG,73,"Nakasongola" +UG,74,"Sembabule" +UG,76,"Tororo" +UG,77,"Arua" +UG,78,"Iganga" +UG,79,"Kabarole" +UG,80,"Kaberamaido" +UG,81,"Kamwenge" +UG,82,"Kanungu" +UG,83,"Kayunga" +UG,84,"Kitgum" +UG,85,"Kyenjojo" +UG,86,"Mayuge" +UG,87,"Mbale" +UG,88,"Moroto" +UG,89,"Mpigi" +UG,90,"Mukono" +UG,91,"Nakapiripirit" +UG,92,"Pader" +UG,93,"Rukungiri" +UG,94,"Sironko" +UG,95,"Soroti" +UG,96,"Wakiso" +UG,97,"Yumbe" +US,AA,"Armed Forces Americas" +US,AE,"Armed Forces Europe, Middle East, & Canada" +US,AK,"Alaska" +US,AL,"Alabama" +US,AP,"Armed Forces Pacific" +US,AR,"Arkansas" +US,AS,"American Samoa" +US,AZ,"Arizona" +US,CA,"California" +US,CO,"Colorado" +US,CT,"Connecticut" +US,DC,"District of Columbia" +US,DE,"Delaware" +US,FL,"Florida" +US,FM,"Federated States of Micronesia" +US,GA,"Georgia" +US,GU,"Guam" +US,HI,"Hawaii" +US,IA,"Iowa" +US,ID,"Idaho" +US,IL,"Illinois" +US,IN,"Indiana" +US,KS,"Kansas" +US,KY,"Kentucky" +US,LA,"Louisiana" +US,MA,"Massachusetts" +US,MD,"Maryland" +US,ME,"Maine" +US,MH,"Marshall Islands" +US,MI,"Michigan" +US,MN,"Minnesota" +US,MO,"Missouri" +US,MP,"Northern Mariana Islands" +US,MS,"Mississippi" +US,MT,"Montana" +US,NC,"North Carolina" +US,ND,"North Dakota" +US,NE,"Nebraska" +US,NH,"New Hampshire" +US,NJ,"New Jersey" +US,NM,"New Mexico" +US,NV,"Nevada" +US,NY,"New York" +US,OH,"Ohio" +US,OK,"Oklahoma" +US,OR,"Oregon" +US,PA,"Pennsylvania" +US,PW,"Palau" +US,RI,"Rhode Island" +US,SC,"South Carolina" +US,SD,"South Dakota" +US,TN,"Tennessee" +US,TX,"Texas" +US,UT,"Utah" +US,VA,"Virginia" +US,VI,"Virgin Islands" +US,VT,"Vermont" +US,WA,"Washington" +US,WI,"Wisconsin" +US,WV,"West Virginia" +US,WY,"Wyoming" +UY,01,"Artigas" +UY,02,"Canelones" +UY,03,"Cerro Largo" +UY,04,"Colonia" +UY,05,"Durazno" +UY,06,"Flores" +UY,07,"Florida" +UY,08,"Lavalleja" +UY,09,"Maldonado" +UY,10,"Montevideo" +UY,11,"Paysandu" +UY,12,"Rio Negro" +UY,13,"Rivera" +UY,14,"Rocha" +UY,15,"Salto" +UY,16,"San Jose" +UY,17,"Soriano" +UY,18,"Tacuarembo" +UY,19,"Treinta y Tres" +UZ,01,"Andijon" +UZ,02,"Bukhoro" +UZ,03,"Farghona" +UZ,04,"Jizzakh" +UZ,05,"Khorazm" +UZ,06,"Namangan" +UZ,07,"Nawoiy" +UZ,08,"Qashqadaryo" +UZ,09,"Qoraqalpoghiston" +UZ,10,"Samarqand" +UZ,11,"Sirdaryo" +UZ,12,"Surkhondaryo" +UZ,13,"Toshkent" +UZ,14,"Toshkent" +UZ,15,"Jizzax" +VC,01,"Charlotte" +VC,02,"Saint Andrew" +VC,03,"Saint David" +VC,04,"Saint George" +VC,05,"Saint Patrick" +VC,06,"Grenadines" +VE,01,"Amazonas" +VE,02,"Anzoategui" +VE,03,"Apure" +VE,04,"Aragua" +VE,05,"Barinas" +VE,06,"Bolivar" +VE,07,"Carabobo" +VE,08,"Cojedes" +VE,09,"Delta Amacuro" +VE,11,"Falcon" +VE,12,"Guarico" +VE,13,"Lara" +VE,14,"Merida" +VE,15,"Miranda" +VE,16,"Monagas" +VE,17,"Nueva Esparta" +VE,18,"Portuguesa" +VE,19,"Sucre" +VE,20,"Tachira" +VE,21,"Trujillo" +VE,22,"Yaracuy" +VE,23,"Zulia" +VE,24,"Dependencias Federales" +VE,25,"Distrito Federal" +VE,26,"Vargas" +VN,01,"An Giang" +VN,03,"Ben Tre" +VN,05,"Cao Bang" +VN,09,"Dong Thap" +VN,13,"Hai Phong" +VN,20,"Ho Chi Minh" +VN,21,"Kien Giang" +VN,23,"Lam Dong" +VN,24,"Long An" +VN,30,"Quang Ninh" +VN,32,"Son La" +VN,33,"Tay Ninh" +VN,34,"Thanh Hoa" +VN,35,"Thai Binh" +VN,37,"Tien Giang" +VN,39,"Lang Son" +VN,43,"Dong Nai" +VN,44,"Ha Noi" +VN,45,"Ba Ria-Vung Tau" +VN,46,"Binh Dinh" +VN,47,"Binh Thuan" +VN,49,"Gia Lai" +VN,50,"Ha Giang" +VN,52,"Ha Tinh" +VN,53,"Hoa Binh" +VN,54,"Khanh Hoa" +VN,55,"Kon Tum" +VN,58,"Nghe An" +VN,59,"Ninh Binh" +VN,60,"Ninh Thuan" +VN,61,"Phu Yen" +VN,62,"Quang Binh" +VN,63,"Quang Ngai" +VN,64,"Quang Tri" +VN,65,"Soc Trang" +VN,66,"Thua Thien-Hue" +VN,67,"Tra Vinh" +VN,68,"Tuyen Quang" +VN,69,"Vinh Long" +VN,70,"Yen Bai" +VN,71,"Bac Giang" +VN,72,"Bac Kan" +VN,73,"Bac Lieu" +VN,74,"Bac Ninh" +VN,75,"Binh Duong" +VN,76,"Binh Phuoc" +VN,77,"Ca Mau" +VN,78,"Da Nang" +VN,79,"Hai Duong" +VN,80,"Ha Nam" +VN,81,"Hung Yen" +VN,82,"Nam Dinh" +VN,83,"Phu Tho" +VN,84,"Quang Nam" +VN,85,"Thai Nguyen" +VN,86,"Vinh Phuc" +VN,87,"Can Tho" +VN,88,"Dac Lak" +VN,89,"Lai Chau" +VN,90,"Lao Cai" +VN,91,"Dak Nong" +VN,92,"Dien Bien" +VN,93,"Hau Giang" +VU,05,"Ambrym" +VU,06,"Aoba" +VU,07,"Torba" +VU,08,"Efate" +VU,09,"Epi" +VU,10,"Malakula" +VU,11,"Paama" +VU,12,"Pentecote" +VU,13,"Sanma" +VU,14,"Shepherd" +VU,15,"Tafea" +VU,16,"Malampa" +VU,17,"Penama" +VU,18,"Shefa" +WS,02,"Aiga-i-le-Tai" +WS,03,"Atua" +WS,04,"Fa" +WS,05,"Gaga" +WS,06,"Va" +WS,07,"Gagaifomauga" +WS,08,"Palauli" +WS,09,"Satupa" +WS,10,"Tuamasaga" +WS,11,"Vaisigano" +YE,01,"Abyan" +YE,02,"Adan" +YE,03,"Al Mahrah" +YE,04,"Hadramawt" +YE,05,"Shabwah" +YE,06,"Lahij" +YE,07,"Al Bayda'" +YE,08,"Al Hudaydah" +YE,09,"Al Jawf" +YE,10,"Al Mahwit" +YE,11,"Dhamar" +YE,12,"Hajjah" +YE,13,"Ibb" +YE,14,"Ma'rib" +YE,15,"Sa'dah" +YE,16,"San'a'" +YE,17,"Taizz" +YE,18,"Ad Dali" +YE,19,"Amran" +YE,20,"Al Bayda'" +YE,21,"Al Jawf" +YE,22,"Hajjah" +YE,23,"Ibb" +YE,24,"Lahij" +YE,25,"Taizz" +ZA,01,"North-Western Province" +ZA,02,"KwaZulu-Natal" +ZA,03,"Free State" +ZA,05,"Eastern Cape" +ZA,06,"Gauteng" +ZA,07,"Mpumalanga" +ZA,08,"Northern Cape" +ZA,09,"Limpopo" +ZA,10,"North-West" +ZA,11,"Western Cape" +ZM,01,"Western" +ZM,02,"Central" +ZM,03,"Eastern" +ZM,04,"Luapula" +ZM,05,"Northern" +ZM,06,"North-Western" +ZM,07,"Southern" +ZM,08,"Copperbelt" +ZM,09,"Lusaka" +ZW,01,"Manicaland" +ZW,02,"Midlands" +ZW,03,"Mashonaland Central" +ZW,04,"Mashonaland East" +ZW,05,"Mashonaland West" +ZW,06,"Matabeleland North" +ZW,07,"Matabeleland South" +ZW,08,"Masvingo" +ZW,09,"Bulawayo" +ZW,10,"Harare" diff --git a/db/geodata/us_region.csv b/db/geodata/us_region.csv deleted file mode 100644 index b8a27ee2e..000000000 --- a/db/geodata/us_region.csv +++ /dev/null @@ -1,57 +0,0 @@ -AA,Armed Forces America -AE,Armed Forces -AP,Armed Forces Pacific -AK,Alaska -AL,Alabama -AR,Arkansas -AZ,Arizona -CA,California -CO,Colorado -CT,Connecticut -DC,District of Columbia -DE,Delaware -FL,Florida -GA,Georgia -GU,Guam -HI,Hawaii -IA,Iowa -ID,Idaho -IL,Illinois -IN,Indiana -KS,Kansas -KY,Kentucky -LA,Louisiana -MA,Massachusetts -MD,Maryland -ME,Maine -MI,Michigan -MN,Minnesota -MO,Missouri -MS,Mississippi -MT,Montana -NC,North Carolina -ND,North Dakota -NE,Nebraska -NH,New Hampshire -NJ,New Jersey -NM,New Mexico -NV,Nevada -NY,New York -OH,Ohio -OK,Oklahoma -OR,Oregon -PA,Pennsylvania -PR,Puerto Rico -RI,Rhode Island -SC,South Carolina -SD,South Dakota -TN,Tennessee -TX,Texas -UT,Utah -VA,Virginia -VI,Virgin Islands -VT,Vermont -WA,Washington -WI,Wisconsin -WV,West Virginia -WY,Wyoming diff --git a/db/manifest b/db/manifest index 954551854..d4b3013d5 100755 --- a/db/manifest +++ b/db/manifest @@ -161,4 +161,11 @@ scheduled_sessions_2.sql scheduled_sessions_3.sql scheduled_sessions_cancel_all.sql scheduled_sessions_started_at.sql -scheduled_sessions_open_rsvps.sql \ No newline at end of file +scheduled_sessions_open_rsvps.sql +add_last_jam_user_fields.sql +remove_lat_lng_user_fields.sql +update_get_work_for_larger_radius.sql +periodic_emails.sql +remember_extra_scoring_data.sql +indexing_for_regions.sql +latency_tester.sql diff --git a/db/up/add_last_jam_user_fields.sql b/db/up/add_last_jam_user_fields.sql new file mode 100644 index 000000000..5542ec4c7 --- /dev/null +++ b/db/up/add_last_jam_user_fields.sql @@ -0,0 +1,8 @@ +ALTER TABLE users ADD COLUMN last_jam_addr BIGINT; + +ALTER TABLE users ADD COLUMN last_jam_locidispid BIGINT; + +-- (j)oin session as musician, (r)egister, (f)tue, (n)etwork test +ALTER TABLE users ADD COLUMN last_jam_updated_reason CHAR(1); + +ALTER TABLE users ADD COLUMN last_jam_updated_at TIMESTAMP; diff --git a/db/up/indexing_for_regions.sql b/db/up/indexing_for_regions.sql new file mode 100644 index 000000000..e59ebaca2 --- /dev/null +++ b/db/up/indexing_for_regions.sql @@ -0,0 +1,2 @@ +create index regions_countrycode_ndx on regions (countrycode); +create unique index regions_countrycode_region_ndx on regions (countrycode, region); diff --git a/db/up/latency_tester.sql b/db/up/latency_tester.sql new file mode 100644 index 000000000..000126654 --- /dev/null +++ b/db/up/latency_tester.sql @@ -0,0 +1,8 @@ +CREATE TABLE latency_testers ( + id VARCHAR(64) PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), + client_id VARCHAR(64) UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +ALTER TABLE connections ALTER COLUMN user_id DROP NOT NULL; \ No newline at end of file diff --git a/db/up/periodic_emails.sql b/db/up/periodic_emails.sql new file mode 100644 index 000000000..ed7d091f9 --- /dev/null +++ b/db/up/periodic_emails.sql @@ -0,0 +1,15 @@ +ALTER TABLE email_batches ADD COLUMN type VARCHAR(64) NOT NULL DEFAULT 'JamRuby::EmailBatch'; +ALTER TABLE email_batches ADD COLUMN sub_type VARCHAR(64); + +ALTER TABLE email_batches ALTER COLUMN body DROP NOT NULL; +ALTER TABLE email_batches ALTER COLUMN subject DROP NOT NULL; + +ALTER TABLE email_batch_sets ADD COLUMN trigger_index INTEGER NOT NULL DEFAULT 0; +ALTER TABLE email_batch_sets ADD COLUMN sub_type VARCHAR(64); +ALTER TABLE email_batch_sets ADD COLUMN user_id VARCHAR(64); + +CREATE INDEX email_batch_sets_progress_idx ON email_batch_sets(user_id, sub_type); +CREATE INDEX users_musician_email_idx ON users(subscribe_email, musician); + +UPDATE users set first_social_promoted_at = first_liked_us; +ALTER TABLE users DROP column first_liked_us; \ No newline at end of file diff --git a/db/up/remember_extra_scoring_data.sql b/db/up/remember_extra_scoring_data.sql new file mode 100644 index 000000000..4ada6f2f3 --- /dev/null +++ b/db/up/remember_extra_scoring_data.sql @@ -0,0 +1,3 @@ +-- add column to hold the raw scoring data that the client posted. + +alter table scores add column scoring_data varchar(4000); diff --git a/db/up/remove_lat_lng_user_fields.sql b/db/up/remove_lat_lng_user_fields.sql new file mode 100644 index 000000000..c15026ee3 --- /dev/null +++ b/db/up/remove_lat_lng_user_fields.sql @@ -0,0 +1,2 @@ +alter table users drop column lat; +alter table users drop column lng; diff --git a/db/up/update_get_work_for_larger_radius.sql b/db/up/update_get_work_for_larger_radius.sql new file mode 100644 index 000000000..f2edb5123 --- /dev/null +++ b/db/up/update_get_work_for_larger_radius.sql @@ -0,0 +1,16 @@ +DROP FUNCTION get_work (mylocidispid BIGINT); +CREATE FUNCTION get_work (mylocidispid BIGINT, myaddr BIGINT) RETURNS TABLE (client_id VARCHAR(64)) ROWS 5 VOLATILE 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), 4023360)); + 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' AND addr != myaddr; + 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; +$$ LANGUAGE plpgsql; diff --git a/monitor/.rspec b/monitor/.rspec new file mode 100755 index 000000000..5f1647637 --- /dev/null +++ b/monitor/.rspec @@ -0,0 +1,2 @@ +--color +--format progress diff --git a/monitor/Gemfile b/monitor/Gemfile new file mode 100755 index 000000000..6b198c71c --- /dev/null +++ b/monitor/Gemfile @@ -0,0 +1,11 @@ +source "https://rubygems.org" + +gem "rspec" +gem "capybara" +gem "capybara-screenshot" +gem "poltergeist" +gem "launchy" # used for opening pages/screenshots when debugging + +# these used only for the Fixnum#seconds method :-/ +gem "i18n" +gem "activesupport" diff --git a/monitor/Gemfile.lock b/monitor/Gemfile.lock new file mode 100755 index 000000000..9ccd01cdd --- /dev/null +++ b/monitor/Gemfile.lock @@ -0,0 +1,59 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (3.1.12) + multi_json (~> 1.0) + addressable (2.3.6) + capybara (2.2.1) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) + capybara-screenshot (0.3.19) + capybara (>= 1.0, < 3) + launchy + cliver (0.3.2) + diff-lcs (1.2.5) + i18n (0.6.9) + launchy (2.4.2) + addressable (~> 2.3) + mime-types (2.2) + mini_portile (0.5.3) + multi_json (1.10.0) + nokogiri (1.6.1) + mini_portile (~> 0.5.0) + nokogiri (1.6.1-x86-mingw32) + mini_portile (~> 0.5.0) + poltergeist (1.5.0) + capybara (~> 2.1) + cliver (~> 0.3.1) + multi_json (~> 1.0) + websocket-driver (>= 0.2.0) + rack (1.5.2) + rack-test (0.6.2) + rack (>= 1.0) + rspec (2.14.1) + rspec-core (~> 2.14.0) + rspec-expectations (~> 2.14.0) + rspec-mocks (~> 2.14.0) + rspec-core (2.14.8) + rspec-expectations (2.14.5) + diff-lcs (>= 1.1.3, < 2.0) + rspec-mocks (2.14.6) + websocket-driver (0.3.3) + xpath (2.0.0) + nokogiri (~> 1.3) + +PLATFORMS + ruby + x86-mingw32 + +DEPENDENCIES + activesupport + capybara + capybara-screenshot + i18n + launchy + poltergeist + rspec diff --git a/web/spec/features/production_spec.rb b/monitor/spec/production_spec.rb old mode 100644 new mode 100755 similarity index 84% rename from web/spec/features/production_spec.rb rename to monitor/spec/production_spec.rb index 3e96f53d0..bfe16d4f2 --- a/web/spec/features/production_spec.rb +++ b/monitor/spec/production_spec.rb @@ -1,9 +1,14 @@ require 'spec_helper' -# these tests MUST be idempotent and DO use actual production user accounts on www -www = 'http://www.jamkazam.com' +# these tests should be idempotent, and not spammy to other JK users +# because they DO use actual production user accounts on www (see the TestUsers below) -describe "Production site at #{www}", :test_www => true, :js => true, :type => :feature, :capybara_feature => true do +# Jenkins executes rspec on this folder every 15 minutes or so. +# SO don't use this to test something like a public session unless you want all the world to see + +www = ENV['MONITOR_URL'] || 'http://www.jamkazam.com' + +describe "Deployed site at #{www}", :js => true, :type => :feature, :capybara_feature => true do subject { page } @@ -12,7 +17,6 @@ describe "Production site at #{www}", :test_www => true, :js => true, :type => Capybara.current_driver = Capybara.javascript_driver Capybara.app_host = www Capybara.run_server = false - Capybara.default_wait_time = 10 end TestUser = Class.new do @@ -28,7 +32,6 @@ describe "Production site at #{www}", :test_www => true, :js => true, :type => first_name + ' ' + last_name end end - user1 = TestUser.new({ email: 'anthony+jim@jamkazam.com', password: 'j4m!t3st3r', first_name: 'Jim', last_name: 'Smith', id: '68e8eea2-140d-44c1-b711-10d07ce70f96' }) user2 = TestUser.new({ email: 'anthony+john@jamkazam.com', password: 'j4m!t3st3r', first_name: 'John', last_name: 'Jones', id: '5bbcf689-2f73-452d-815a-c4f44e9e7f3e' }) @@ -46,7 +49,7 @@ describe "Production site at #{www}", :test_www => true, :js => true, :type => end it "is possible for #{user1} and #{user2} to see each other online, and to send messages" do - # this example heavily based on text_message_spec.rb + # this example heavily based on text_message_spec.rb in 'web' in_client(user1) do sign_in_poltergeist(user1) @@ -89,4 +92,3 @@ describe "Production site at #{www}", :test_www => true, :js => true, :type => end end - diff --git a/monitor/spec/spec_helper.rb b/monitor/spec/spec_helper.rb new file mode 100755 index 000000000..5b3882d80 --- /dev/null +++ b/monitor/spec/spec_helper.rb @@ -0,0 +1,50 @@ +require 'rubygems' +require 'active_support/time' +require 'capybara' +require 'capybara/rspec' +require 'capybara-screenshot' +require 'capybara-screenshot/rspec' +require 'capybara/poltergeist' + +require 'support/client_interactions' #TODO: Strip out the helper methods that production_spec does not use +require 'support/utilities' +require 'support/stubs' # to make the JamXXXX warnings go away + +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# Require this file using `require "spec_helper"` to ensure that it is only +# loaded once. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + config.treat_symbols_as_metadata_keys_with_true_values = true + config.run_all_when_everything_filtered = true + config.filter_run :focus + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = 'random' + + config.include Capybara::DSL + + config.before(:each) do + page.driver.headers = { 'User-Agent' => 'monitor' } + end +end + +#Capybara.register_driver :poltergeist do |app| +# Capybara::Poltergeist::Driver.new(app, { phantomjs_logger: File.open('console.log', 'w') }) +#end +Capybara.javascript_driver = :poltergeist +Capybara.default_driver = :poltergeist +Capybara.run_server = false # since we're testing an app outside this project +Capybara.default_wait_time = 15 # ^^ ditto + +Capybara.configure do |config| + config.match = :one + config.exact_options = true + config.ignore_hidden_elements = true + config.visible_text_only = true +end \ No newline at end of file diff --git a/monitor/spec/support/client_interactions.rb b/monitor/spec/support/client_interactions.rb new file mode 100755 index 000000000..d2f000475 --- /dev/null +++ b/monitor/spec/support/client_interactions.rb @@ -0,0 +1,126 @@ + +# methods here all assume you are in /client + +NOTIFICATION_PANEL = '[layout-id="panelNotifications"]' + +# enters text into the search sidebar +def site_search(text, options = {}) + within('#searchForm') do + fill_in "search-input", with: text + end + + if options[:expand] + page.driver.execute_script("jQuery('#searchForm').submit()") + find('h1', text:'search results') + end +end + +# goes to the musician tile, and tries to find a musician +def find_musician(user) + visit "/client#/musicians" + + timeout = 30 + + start = Time.now + # scroll by 100px until we find a user with the right id + while page.all('#end-of-musician-list').length == 0 + page.execute_script('jQuery("#musician-filter-results").scrollTo("+=100px", 0, {axis:"y"})') + found = page.all(".result-list-button-wrapper[data-musician-id='#{user.id}']") + if found.length == 1 + return found[0] + elsif found.length > 1 + raise "ambiguous results in musician list" + end + + if Time.now - start > timeout + raise "unable to find musician #{user} within #{timeout} seconds" + end + end + + raise "unable to find musician #{user}" +end + +def initiate_text_dialog(user) + + # verify that the chat window is grayed out + site_search(user.first_name, expand: true) + + find("#search-results a[user-id=\"#{user.id}\"][hoveraction=\"musician\"]", text: user.name).hover_intent + find('#musician-hover #btnMessage').trigger(:click) + find('h1', text: 'conversation with ' + user.name) +end + +# sends a text message in the chat interface. +def send_text_message(msg, options={}) + find('#text-message-dialog') # assert that the dialog is showing already + + within('#text-message-dialog form.text-message-box') do + fill_in 'new-text-message', with: msg + end + find('#text-message-dialog .btn-send-text-message').trigger(:click) + find('#text-message-dialog .previous-message-text', text: msg) unless options[:should_fail] + + # close the dialog if caller specified close_on_send + if options[:close_on_send] + find('#text-message-dialog .btn-close-dialog', text: 'CLOSE').trigger(:click) if options[:close_on_send] + page.should have_no_selector('#text-message-dialog') + end + + if options[:should_fail] + find('#notification').should have_text(options[:should_fail]) + end +end + +# sends a chat message during session +def send_chat_message(msg) + find("[layout-id=\"panelChat\"] .chat-sender").should be_visible + + within("[layout-id=\"panelChat\"] .chat-sender form.chat-message-form") do + fill_in 'new-chat-message', with: msg + end + find("[layout-id=\"panelChat\"] .chat-sender .btn-send-chat-message").trigger(:click) +end + +def open_notifications + find("#{NOTIFICATION_PANEL} .panel-header").trigger(:click) +end + + +def hover_intent(element) + element.hover + element.hover +end + +# forces document.hasFocus() to return false +def document_blur + page.evaluate_script(%{(function() { + // save original + if(!window.documentFocus) { window.documentFocus = window.document.hasFocus; } + + window.document.hasFocus = function() { + console.log("document.hasFocus() returns false"); + return false; + } + })()}) +end + +def document_focus + page.evaluate_script(%{(function() { + // save original + if(!window.documentFocus) { window.documentFocus = window.document.hasFocus; } + + window.document.hasFocus = function() { + console.log("document.hasFocus() returns true"); + return true; + } + })()}) +end + +# simulates focus event on window +def window_focus + page.evaluate_script(%{window.jQuery(window).trigger('focus');}) +end + +def close_websocket + page.evaluate_script("window.JK.JamServer.close(true)") +end \ No newline at end of file diff --git a/monitor/spec/support/stubs.rb b/monitor/spec/support/stubs.rb new file mode 100755 index 000000000..86d78ff3e --- /dev/null +++ b/monitor/spec/support/stubs.rb @@ -0,0 +1,9 @@ +module JamRuby + class User + + end +end + +def signin_path + '/signin' +end \ No newline at end of file diff --git a/monitor/spec/support/utilities.rb b/monitor/spec/support/utilities.rb new file mode 100755 index 000000000..9003be855 --- /dev/null +++ b/monitor/spec/support/utilities.rb @@ -0,0 +1,481 @@ + +# add a hover_intent method to element, so that you can do find(selector).hover_intent +module Capybara + module Node + class Element + + def attempt_hover + begin + hover + rescue => e + end + end + def hover_intent + hover + sleep 0.3 + attempt_hover + sleep 0.3 + attempt_hover + end + end + end +end + + + +# holds a single test's session name's, mapped to pooled session names +$capybara_session_mapper = {} + +# called in before (or after) test, to make sure each test run has it's own map of session names +def reset_session_mapper + $capybara_session_mapper.clear + + Capybara.session_name = :default +end + +# manages the mapped session name +def mapped_session_name(session_name) + return :default if session_name == :default # special treatment for the built-in session + $capybara_session_mapper[session_name] ||= 'session_' + $capybara_session_mapper.length.to_s +end + +# in place of ever using Capybara.session_name directly, +# this utility is used to handle the mapping of session names in a way across all tests runs +def in_client(name) + session_name = name.class == JamRuby::User ? name.id : name + + Capybara.session_name = mapped_session_name(session_name) + + yield +end + + +def cookie_jar + Capybara.current_session.driver.browser.current_session.instance_variable_get(:@rack_mock_session).cookie_jar +end + + +#see also ruby/spec/support/utilities.rb +JAMKAZAM_TESTING_BUCKET = 'jamkazam-testing' #at least, this is the name given in jam-ruby +def wipe_s3_test_bucket + s3 = AWS::S3.new(:access_key_id => Rails.application.config.aws_access_key_id, + :secret_access_key => Rails.application.config.aws_secret_access_key) + test_bucket = s3.buckets[JAMKAZAM_TESTING_BUCKET] + if test_bucket.name == JAMKAZAM_TESTING_BUCKET + test_bucket.objects.each do |obj| + obj.delete + end + end +end + + +def sign_in(user) + visit signin_path + fill_in "Email", with: user.email + fill_in "Password", with: user.password + click_button "SIGN IN" + # Sign in when not using Capybara as well. + cookie_jar[:remember_token] = user.remember_token +end + +def set_cookie(k, v) + case Capybara.current_session.driver + when Capybara::Poltergeist::Driver + page.driver.set_cookie(k,v) + when Capybara::RackTest::Driver + headers = {} + Rack::Utils.set_cookie_header!(headers,k,v) + cookie_string = headers['Set-Cookie'] + Capybara.current_session.driver.browser.set_cookie(cookie_string) + when Capybara::Selenium::Driver + page.driver.browser.manage.add_cookie(:name=>k, :value=>v) + else + raise "no cookie-setter implemented for driver #{Capybara.current_session.driver.class.name}" + end +end + +def sign_in_poltergeist(user, options = {}) + validate = options[:validate] + validate = true if validate.nil? + + visit signin_path + fill_in "Email Address:", with: user.email + fill_in "Password:", with: user.password + click_button "SIGN IN" + + wait_until_curtain_gone + + # presence of this means websocket gateway is not working + page.should have_no_selector('.no-websocket-connection') if validate +end + + +def sign_out() + if Capybara.javascript_driver == :poltergeist + page.driver.remove_cookie(:remember_token) + else + page.driver.browser.manage.remove_cookie :name => :remember_token + end +end + +def sign_out_poltergeist(options = {}) + find('.userinfo').hover() + click_link 'Sign Out' + should_be_at_root if options[:validate] +end + +def should_be_at_root + find('h1', text: 'Play music together over the Internet as if in the same room') +end + +def leave_music_session_sleep_delay + # add a buffer to ensure WSG has enough time to expire + sleep_dur = (Rails.application.config.websocket_gateway_connect_time_stale_browser + + Rails.application.config.websocket_gateway_connect_time_expire_browser) * 1.4 + sleep sleep_dur +end + + +def wait_for_ajax(wait=Capybara.default_wait_time) + wait = wait * 10 #(because we sleep .1) + + counter = 0 + while page.execute_script("$.active").to_i > 0 + counter += 1 + sleep(0.1) + raise "AJAX request took longer than #{wait} seconds." if counter >= wait + end +end + +# waits until the user object has been requested, which comes after the 'curtain' is lifted +# and after a call to /api/user/:id for the current user is called initially +def wait_until_user(wait=Capybara.default_wait_time) + wait = wait * 10 #(because we sleep .1) + + counter = 0 + # while page.execute_script("$('.curtain').is(:visible)") == "true" + # counter += 1 + # sleep(0.1) + # raise "Waiting for user to populate took longer than #{wait} seconds." if counter >= wait + # end +end + +def wait_until_curtain_gone + page.should have_no_selector('.curtain') +end + +def wait_to_see_my_track + within('div.session-mytracks') {first('div.session-track.track')} +end + +def repeat_for(duration=Capybara.default_wait_time) + finish_time = Time.now + duration.seconds + loop do + yield + sleep 1 # by default this will execute the block every 1 second + break if (Time.now > finish_time) + end +end + +def determine_test_name(metadata, test_name_buffer = '') + description = metadata[:description_args] + if description.kind_of?(Array) + description = description[0] + end + if metadata.has_key? :example_group + return determine_test_name(metadata[:example_group], "#{description} #{test_name_buffer}") + else + return "#{description} #{test_name_buffer}" + end +end + +def get_description + description = example.metadata[:description_args] + if description.kind_of?(Array) + description = description[0] + end + return description +end + +# will select the value from a easydropdown'ed select element +def jk_select(text, select) + + # the approach here is to find the hidden select element, and work way back up to the elements that need to be interacted with + find(select, :visible => false).find(:xpath, 'ancestor::div[contains(@class, "dropdown easydropdown")]').trigger(:click) + find(select, :visible => false).find(:xpath, 'ancestor::div[contains(@class, "dropdown-wrapper") and contains(@class, "easydropdown-wrapper") and contains(@class, "open")]').find('li', text: text).trigger(:click) + + # works, but is 'cheating' because of visible = false + #select(genre, :from => 'genres', :visible => false) +end + +# takes, or creates, a unique session description which is returned for subsequent calls to join_session to use +# in finding this session) +def create_session(options={}) + creator = options[:creator] || FactoryGirl.create(:user) + unique_session_desc = options[:description] || "create_join_session #{SecureRandom.urlsafe_base64}" + genre = options[:genre] || 'Rock' + musician_access = options[:musician_access].nil? ? true : options[:musician_access] + fan_access = options[:fan_access].nil? ? true : options[:fan_access] + + # create session in one client + in_client(creator) do + page.driver.resize(1500, 800) # makes sure all the elements are visible + emulate_client + sign_in_poltergeist creator + wait_until_curtain_gone + visit "/client#/createSession" + expect(page).to have_selector('h2', text: 'session info') + + within('#create-session-form') do + fill_in('description', :with => unique_session_desc) + #select(genre, :from => 'genres', :visible => false) # this works, but is 'cheating' because easydropdown hides the native select element + jk_select(genre, '#create-session-form select[name="genres"]') + + jk_select(musician_access ? 'Public' : 'Private', '#create-session-form select#musician-access') + jk_select(fan_access ? 'Public' : 'Private', '#create-session-form select#fan-access') + find('#create-session-form div.musician-access-false.iradio_minimal').trigger(:click) + find('div.intellectual-property ins').trigger(:click) + find('#btn-create-session').trigger(:click) # fails if page width is low + end + + # verify that the in-session page is showing + expect(page).to have_selector('h2', text: 'my tracks') + find('#session-screen .session-mytracks .session-track') + end + + return creator, unique_session_desc, genre + +end + +# this code assumes that there are no music sessions in the database. it should fail on the +# find('.join-link') call if > 1 session exists because capybara will complain of multiple matches +def join_session(joiner, options) + description = options[:description] + + in_client(joiner) do + page.driver.resize(1500, 800) # makes sure all the elements are visible + emulate_client + sign_in_poltergeist joiner + wait_until_curtain_gone + visit "/client#/findSession" + + # verify the session description is seen by second client + expect(page).to have_text(description) + find('.join-link').trigger(:click) + find('#btn-accept-terms').trigger(:click) + expect(page).to have_selector('h2', text: 'my tracks') + find('#session-screen .session-mytracks .session-track') + end +end + + + +def emulate_client + page.driver.headers = { 'User-Agent' => ' JamKazam ' } +end + +def create_join_session(creator, joiners=[], options={}) + options[:creator] = creator + creator, unique_session_desc = create_session(options) + + # find session in second client + joiners.each do |joiner| + join_session(joiner, description: unique_session_desc) + end + + return creator, unique_session_desc +end + +def formal_leave_by user + in_client(user) do + find('#session-leave').trigger(:click) + #find('#btn-accept-leave-session').trigger(:click) + expect(page).to have_selector('h2', text: 'feed') + end +end + +def start_recording_with(creator, joiners=[], genre=nil) + create_join_session(creator, joiners, {genre: genre}) + in_client(creator) do + find('#recording-start-stop').trigger(:click) + find('#recording-status').should have_content 'Stop Recording' + end + joiners.each do |joiner| + in_client(joiner) do + find('#notification').should have_content 'started a recording' + find('#recording-status').should have_content 'Stop Recording' + end + end +end + +def stop_recording + find('#recording-start-stop').trigger(:click) +end + +def assert_recording_finished + find('#recording-status').should have_content 'Make a Recording' + should have_selector('h1', text: 'recording finished') +end + +def check_recording_finished_for(users=[]) + users.each do |user| + in_client(user) do + assert_recording_finished + end + end +end + +def claim_recording(name, description) + find('#recording-finished-dialog h1') + fill_in "claim-recording-name", with: name + fill_in "claim-recording-description", with: description + find('#keep-session-recording').trigger(:click) + page.should have_no_selector('h1', text: 'recording finished') +end + +def set_session_as_private() + find('#session-settings-button').trigger(:click) + within('#session-settings-dialog') do + jk_select("Private", '#session-settings-dialog #session-settings-musician-access') + #select('Private', :from => 'session-settings-musician-access') + find('#session-settings-dialog-submit').trigger(:click) + end + # verify it's dismissed + page.should have_no_selector('h1', text: 'update session settings') +end + +def set_session_as_public() + find('#session-settings-button').trigger(:click) + within('#session-settings-dialog') do + jk_select("Public", '#session-settings-dialog #session-settings-musician-access') + # select('Public', :from => 'session-settings-musician-access') + find('#session-settings-dialog-submit').trigger(:click) + end + # verify it's dismissed + page.should have_no_selector('h1', text: 'update session settings') +end + +def get_options(selector) + find(selector, :visible => false).all('option', :visible => false).collect(&:text).uniq +end + +def selected_genres(selector='#session-settings-genre') + page.evaluate_script("JK.GenreSelectorHelper.getSelectedGenres('#{selector}')") +end + +def random_genre + ['African', + 'Ambient', + 'Asian', + 'Blues', + 'Classical', + 'Country', + 'Electronic', + 'Folk', + 'Hip Hop', + 'Jazz', + 'Latin', + 'Metal', + 'Pop', + 'R&B', + 'Reggae', + 'Religious', + 'Rock', + 'Ska', + 'Other'].sample +end + +def change_session_genre #randomly just change it + here = 'select.genre-list' + #wait_for_ajax + find('#session-settings-button').trigger(:click) + find('#session-settings-dialog') # ensure the dialog is visible + within('#session-settings-dialog') do + wait_for_ajax + @new_genre = get_options(here).-(["Select Genre"]).-(selected_genres).sample.to_s + jk_select(@new_genre, '#session-settings-dialog select[name="genres"]') + wait_for_ajax + find('#session-settings-dialog-submit').trigger(:click) + end + return @new_genre +end + +def get_session_genre + here = 'select.genre-list' + find('#session-settings-button').trigger(:click) + wait_for_ajax + @current_genres = selected_genres + find('#session-settings-dialog-submit').trigger(:click) + return @current_genres.join(" ") +end + +def find_session_contains?(text) + visit "/client#/findSession" + wait_for_ajax + within('#find-session-form') do + expect(page).to have_text(text) + end +end + +def assert_all_tracks_seen(users=[]) + users.each do |user| + in_client(user) do + users.reject {|u| u==user}.each do |other| + find('div.track-label', text: other.name) + #puts user.name + " is able to see " + other.name + "\'s track" + end + end + end +end + +def view_profile_of user + id = user.kind_of?(JamRuby::User) ? user.id : user + # assume some user signed in already + visit "/client#/profile/#{id}" + wait_until_curtain_gone +end + +def view_band_profile_of band + id = band.kind_of?(JamRuby::Band) ? band.id : + band.kind_of?(JamRuby::BandMusician) ? band.bands.first.id : band + visit "/client#/bandProfile/#{id}" + wait_until_curtain_gone +end + +def sidebar_search_for string, category + visit "/client#/home" + find('#search-input') + fill_in "search", with: string + sleep 1 + page.execute_script("JK.Sidebar.searchForInput()") + wait_for_ajax + jk_select(category, "search_text_type") + wait_for_ajax +end + +def show_user_menu + page.execute_script("$('ul.shortcuts').show()") + #page.execute_script("JK.UserDropdown.menuHoverIn()") +end + +# wait for the easydropdown version of the specified select element to become visible +def wait_for_easydropdown(select) + find(select, :visible => false).find(:xpath, 'ancestor::div[contains(@class, "dropdown easydropdown")]') +end + +# defaults to enter key (13) +def send_key(selector, keycode = 13) + keypress_script = "var e = $.Event('keyup', { keyCode: #{keycode} }); jQuery('#{selector}').trigger(e);" + page.driver.execute_script(keypress_script) + +end + +def special_characters + ["?", "[", "]", "/", "\\", "=", "<", ">", ":", ";", ",", "'", "\"", "&", "$", "#", "*", "(", ")", "|", "~", "`", "!", "{", "}"] +end + +def garbage length + output = '' + length.times { output << special_characters.sample } + output.slice(0, length) +end \ No newline at end of file diff --git a/resetdb.sh b/resetdb.sh new file mode 100755 index 000000000..71275da3a --- /dev/null +++ b/resetdb.sh @@ -0,0 +1,8 @@ +#!/bin/sh -x +dropdb --if-exists jam +dropdb --if-exists jam_db_build +dropdb --if-exists jam_ruby_test +dropdb --if-exists jam_web_test +dropdb --if-exists jam_websockets_test +createdb -Upostgres jam || sudo su postgres -c "createdb jam" + diff --git a/ruby/Gemfile b/ruby/Gemfile index 7f2d5b796..6c4e9277e 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -4,7 +4,7 @@ unless ENV["LOCAL_DEV"] == "1" source 'https://jamjam:blueberryjam@int.jamkazam.com/gems/' end -devenv = ENV["BUILD_NUMBER"].nil? || ENV["TEST_WWW"] == "1" +devenv = ENV["BUILD_NUMBER"].nil? if devenv gem 'jam_db', :path=> "../db/target/ruby_package" @@ -57,6 +57,7 @@ group :test do gem 'faker' gem 'resque_spec' #, :path => "/home/jam/src/resque_spec/" gem 'timecop' + gem 'rspec-prof' end # Specify your gem's dependencies in jam_ruby.gemspec diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index ae2c1717e..141cff16b 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -42,7 +42,11 @@ require "jam_ruby/resque/scheduled/icecast_config_retry" require "jam_ruby/resque/scheduled/icecast_source_check" require "jam_ruby/resque/scheduled/cleanup_facebook_signup" require "jam_ruby/resque/scheduled/unused_music_notation_cleaner" +require "jam_ruby/resque/scheduled/user_progress_emailer" +require "jam_ruby/resque/scheduled/daily_session_emailer" +require "jam_ruby/resque/scheduled/new_musician_emailer" require "jam_ruby/resque/google_analytics_event" +require "jam_ruby/resque/batch_email_job" require "jam_ruby/mq_router" require "jam_ruby/base_manager" require "jam_ruby/connection_manager" @@ -82,6 +86,7 @@ require "jam_ruby/models/band_invitation" require "jam_ruby/models/band_musician" require "jam_ruby/models/connection" require "jam_ruby/models/diagnostic" +require "jam_ruby/models/latency_tester" require "jam_ruby/models/friendship" require "jam_ruby/models/active_music_session" require "jam_ruby/models/music_session_comment" @@ -145,10 +150,14 @@ require "jam_ruby/models/country" require "jam_ruby/models/region" require "jam_ruby/models/city" require "jam_ruby/models/email_batch" +require "jam_ruby/models/email_batch_periodic" +require "jam_ruby/models/email_batch_new_musician" +require "jam_ruby/models/email_batch_progression" +require "jam_ruby/models/email_batch_scheduled_sessions" require "jam_ruby/models/email_batch_set" -require "jam_ruby/models/email_error" 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/chat_message" diff --git a/ruby/lib/jam_ruby/app/mailers/batch_mailer.rb b/ruby/lib/jam_ruby/app/mailers/batch_mailer.rb index 17f238090..19a0ea794 100644 --- a/ruby/lib/jam_ruby/app/mailers/batch_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/batch_mailer.rb @@ -1,5 +1,6 @@ module JamRuby - class BatchMailer < JamRuby::AsyncMailer + class BatchMailer < ActionMailer::Base + include SendGrid layout "user_mailer" sendgrid_category :use_subject_lines @@ -23,10 +24,10 @@ module JamRuby end end - def send_batch_email(batch_id, user_ids) - users = User.find_all_by_id(user_ids) + def send_batch_email(batch_id, user_id) + user = User.find_by_id(user_id) batch = EmailBatch.find(batch_id) - self._send_batch(batch, users) + self._send_batch(batch, [user]) end def send_batch_email_test(batch_id) diff --git a/ruby/lib/jam_ruby/app/mailers/progress_mailer.rb b/ruby/lib/jam_ruby/app/mailers/progress_mailer.rb new file mode 100644 index 000000000..789b50716 --- /dev/null +++ b/ruby/lib/jam_ruby/app/mailers/progress_mailer.rb @@ -0,0 +1,26 @@ +module JamRuby + class ProgressMailer < ActionMailer::Base + include SendGrid + layout "user_mailer" + + sendgrid_category :use_subject_lines + sendgrid_unique_args :env => Environment.mode + default :from => UserMailer::DEFAULT_SENDER + + def send_reminder(batch_set) + user = batch_set.user + + sendgrid_recipients([user.email]) + sendgrid_substitute('@USERID', [user.id]) + sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) + + mail(:to => user.email, + :subject => batch_set.subject, + :title => batch_set.title) do |format| + format.text { render batch_set.sub_type } + format.html { render batch_set.sub_type } + end + end + + end +end diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index 6a5d5ec92..29ddc3e68 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -41,6 +41,7 @@ sendgrid_recipients([user.email]) sendgrid_substitute('@USERID', [user.id]) + sendgrid_substitute(EmailBatchProgression::VAR_FIRST_NAME, [user.first_name]) mail(:to => user.email, :subject => "Welcome to JamKazam") do |format| format.text @@ -101,14 +102,14 @@ end end - def new_musicians(user, new_nearby, host='www.jamkazam.com') - @user, @new_nearby, @host = user, new_nearby, host + def new_musicians(user, new_musicians, host='www.jamkazam.com') + @user, @new_musicians, @host = user, new_musicians, host sendgrid_recipients([user.email]) sendgrid_substitute('@USERID', [user.id]) - sendgrid_unique_args :type => "new_musicians" - mail(:to => user.email, :subject => "JamKazam New Musicians in Your Area") do |format| + + mail(:to => user.email, :subject => EmailBatchNewMusician.subject) do |format| format.text format.html end @@ -222,7 +223,7 @@ subject = "Session Invitation" unique_args = {:type => "scheduled_session_invitation"} @body = msg - @session_name = session.description + @session_name = session.name @session_date = session.scheduled_start @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" sendgrid_category "Notification" @@ -241,7 +242,7 @@ subject = "Session RSVP" unique_args = {:type => "scheduled_session_rsvp"} @body = msg - @session_name = session.description + @session_name = session.name @session_date = session.scheduled_start @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" sendgrid_category "Notification" @@ -260,7 +261,7 @@ subject = "Session RSVP Approved" unique_args = {:type => "scheduled_session_rsvp_approved"} @body = msg - @session_name = session.description + @session_name = session.name @session_date = session.scheduled_start @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" sendgrid_category "Notification" @@ -279,7 +280,7 @@ subject = "Session RSVP Cancelled" unique_args = {:type => "scheduled_session_rsvp_cancelled"} @body = msg - @session_name = session.description + @session_name = session.name @session_date = session.scheduled_start @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" sendgrid_category "Notification" @@ -298,7 +299,7 @@ subject = "Your Session RSVP Cancelled" unique_args = {:type => "scheduled_session_rsvp_cancelled_org"} @body = msg - @session_name = session.description + @session_name = session.name @session_date = session.scheduled_start @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" sendgrid_category "Notification" @@ -317,7 +318,7 @@ subject = "Session Cancelled" unique_args = {:type => "scheduled_session_cancelled"} @body = msg - @session_name = session.description + @session_name = session.name @session_date = session.scheduled_start @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" sendgrid_category "Notification" @@ -336,7 +337,7 @@ subject = "Session Rescheduled" unique_args = {:type => "scheduled_session_rescheduled"} @body = msg - @session_name = session.description + @session_name = session.name @session_date = session.scheduled_start @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" sendgrid_category "Notification" @@ -355,7 +356,7 @@ subject = "Session Rescheduled" unique_args = {:type => "scheduled_session_reminder"} @body = msg - @session_name = session.description + @session_name = session.name @session_date = session.scheduled_start @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" sendgrid_category "Notification" @@ -374,7 +375,7 @@ subject = "New Session Comment" unique_args = {:type => "scheduled_session_comment"} @body = msg - @session_name = session.description + @session_name = session.name @session_date = session.scheduled_start @comment = comment @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" @@ -390,6 +391,24 @@ end end + def scheduled_session_daily(receiver, sessions_and_latency) + sendgrid_category "Notification" + sendgrid_unique_args :type => "scheduled_session_daily" + + sendgrid_recipients([receiver.email]) + sendgrid_substitute('@USERID', [receiver.id]) + + @user = receiver + @sessions_and_latency = sessions_and_latency + + @title = 'New Scheduled Sessions Matched to You' + mail(:to => receiver.email, + :subject => EmailBatchScheduledSessions.subject) do |format| + format.text + format.html + end + end + def band_session_join(email, msg, session_id) subject = "A band that you follow has joined a session" unique_args = {:type => "band_session_join"} diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_dl_notrun.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_dl_notrun.html.erb new file mode 100644 index 000000000..50719e908 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_dl_notrun.html.erb @@ -0,0 +1,14 @@ +<% provide(:title, @title) %> + +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+ +

We noticed that you have downloaded the free JamKazam application, but you have not yet run the app. You can find other musicians and listen to sessions and recordings on our website, but you need to run the JamKazam application to play with other musicians online. If you are having trouble installing or running the app, please visit our JamKazam support center at the link below, and post a request for assistance so that we can help you get up and running. +

+ +

+https://jamkazam.desk.com +

+ +

-- Team JamKazam +

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_dl_notrun.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_dl_notrun.text.erb new file mode 100644 index 000000000..c447c34fb --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_dl_notrun.text.erb @@ -0,0 +1,7 @@ +<%= @title %> + +Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- + +We noticed that you have downloaded the free JamKazam application, but you have not yet run the app. You can find other musicians and listen to sessions and recordings on our website, but you need to run the JamKazam application to play with other musicians online. If you are having trouble installing or running the app, please visit our JamKazam support center at the link below, and post a request for assistance so that we can help you get up and running. + +-- Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.html.erb new file mode 100644 index 000000000..cb221e5c9 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.html.erb @@ -0,0 +1,18 @@ +<% provide(:title, @title) %> + +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+ +

We noticed that you have registered as a JamKazam musician, but you have not yet downloaded and started using the free JamKazam application. You can find other musicians and listen to sessions and recordings on our website, but you need the free JamKazam application to play with other musicians online. Please click the link below to go to the download page for the free JamKazam application, or visit our JamKazam support center so that we can help you get up and running. +

+ +

+Go to Download Page +

+ +

+Go to Support Center +

+ +

-- Team JamKazam +

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.text.erb new file mode 100644 index 000000000..4ec636c7b --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.text.erb @@ -0,0 +1,11 @@ +<%= @title %> + +Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- + +We noticed that you have registered as a JamKazam musician, but you have not yet downloaded and started using the free JamKazam application. You can find other musicians and listen to sessions and recordings on our website, but you need the free JamKazam application to play with other musicians online. Please click the link below to go to the download page for the free JamKazam application, or visit our JamKazam support center so that we can help you get up and running. + +Go to Download Page: http://www.jamkazam.com/downloads + +Go to Support Center: https://jamkazam.desk.com + +-- Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_run_notgear.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_run_notgear.html.erb new file mode 100644 index 000000000..c561dd267 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_run_notgear.html.erb @@ -0,0 +1,19 @@ +<% provide(:title, @title) %> + +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+ +

We noticed that you have not yet successfully set up your audio gear and passed the JamKazam latency and input/output audio gear tests. This means that you cannot yet play in online sessions with other musicians. If you are having trouble with this step, please click the link below for a knowledge base article that can help you get past this hurdle. If the test says your audio gear is not fast enough, or if your audio quality sounds poor, or if you are just confused, it’s very likely the tips in this article will help you get things set up and optimized so you can start playing online. +

+ +

http://bit.ly/1i4Uul4 +

+ +

And if this knowledge base article does not get you fixed up, please visit our JamKazam support center at the link below, and post a request for assistance so that we can help you get up and running: +

+ +

https://jamkazam.desk.com +

+ +

-- Team JamKazam +

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_run_notgear.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_run_notgear.text.erb new file mode 100644 index 000000000..b635ede90 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_run_notgear.text.erb @@ -0,0 +1,13 @@ +<%= @title %> + +Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- + +We noticed that you have not yet successfully set up your audio gear and passed the JamKazam latency and input/output audio gear tests. This means that you cannot yet play in online sessions with other musicians. If you are having trouble with this step, please click the link below for a knowledge base article that can help you get past this hurdle. If the test says your audio gear is not fast enough, or if your audio quality sounds poor, or if you are just confused, it’s very likely the tips in this article will help you get things set up and optimized so you can start playing online. + +http://bit.ly/1i4Uul4 + +And if this knowledge base article does not get you fixed up, please visit our JamKazam support center at the link below, and post a request for assistance so that we can help you get up and running: + +https://jamkazam.desk.com + +-- Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.html.erb new file mode 100644 index 000000000..43be52177 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.html.erb @@ -0,0 +1,31 @@ +<% provide(:title, @title) %> + +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+ +

We noticed that you haven’t yet played in a JamKazam session with multiple musicians that lasted long enough to be productive or fun. Since that’s the whole point of the service, we wanted to reach out to see if we can help you. Please take a quick look at the suggestions below to see if they help you! +

+ +

Find Other Musicians on JamKazam
+It’s still very early in our company’s development, so we don’t have zillions of users online on our service yet. If you click Find Session, you will often not find a good session to join, both due to the number of musicians online at any given time, and also because you won’t see private sessions where groups of musicians don’t want to be interrupted in their sessions. +

+ +

If you are having trouble getting into sessions, we’d suggest you click the Musicians tile on the home screen of the app or the website: Go To Musicians Page +

+ +

This will display the JamKazam musicians sorted by latency to you - in other words, you can see which musicians have good network connections to you. Any musicians with green and yellow latency scores have good enough connections to support a play session with you. We recommend that read the profiles of these musicians to find others with shared musical interests and good network connections to you, and then use the Message button to say hi and see if they are interested in playing with you. If they are, use the Connect button to “friend” them on JamKazam, and use the Message button to set up a time to meet online for a session. +

+ +

Invite Your Friends to Play
+One of the best ways to connect and play with others is to invite your friends from the “real world” to join you on JamKazam, and then set up a time to meet online and get into a session together. To do this, just go to www.jamkazam.com and sign in. Then move your mouse over your picture or name in the upper right corner of the screen. A menu will be displayed. Click the Invite Friends option, and then you can click Facebook, Google, or Email to easily invite your friends from different services to join you on JamKazam. +

+ +

Troubleshoot Session Problems
+If you are having audio quality problems or other issues when you get into a session, please click the link below to visit our support center, and check the knowledge base articles under the Troubleshooting header to find solutions. And if that doesn’t work, please post a request for assistance in the support center so that we can help you get up and running: +

+ +

https://jamkazam.desk.com +

+ +

-- Team JamKazam +

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.text.erb new file mode 100644 index 000000000..d027cf178 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.text.erb @@ -0,0 +1,22 @@ +<%= @title %> + +Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- + +We noticed that you haven’t yet played in a JamKazam session with multiple musicians that lasted long enough to be productive or fun. Since that’s the whole point of the service, we wanted to reach out to see if we can help you. Please take a quick look at the suggestions below to see if they help you! + +Find Other Musicians on JamKazam +It’s still very early in our company’s development, so we don’t have zillions of users online on our service yet. If you click Find Session, you will often not find a good session to join, both due to the number of musicians online at any given time, and also because you won’t see private sessions where groups of musicians don’t want to be interrupted in their sessions. + +If you are having trouble getting into sessions, we’d suggest you click the Musicians tile on the home screen of the app or the website: http://www.jamkazam.com/client#/musicians + +This will display the JamKazam musicians sorted by latency to you - in other words, you can see which musicians have good network connections to you. Any musicians with green and yellow latency scores have good enough connections to support a play session with you. We recommend that read the profiles of these musicians to find others with shared musical interests and good network connections to you, and then use the Message button to say hi and see if they are interested in playing with you. If they are, use the Connect button to “friend” them on JamKazam, and use the Message button to set up a time to meet online for a session. + +Invite Your Friends to Play +One of the best ways to connect and play with others is to invite your friends from the “real world” to join you on JamKazam, and then set up a time to meet online and get into a session together. To do this, just go to www.jamkazam.com and sign in. Then move your mouse over your picture or name in the upper right corner of the screen. A menu will be displayed. Click the Invite Friends option, and then you can click Facebook, Google, or Email to easily invite your friends from different services to join you on JamKazam. + +Troubleshoot Session Problems +If you are having audio quality problems or other issues when you get into a session, please click the link below to visit our support center, and check the knowledge base articles under the Troubleshooting header to find solutions. And if that doesn’t work, please post a request for assistance in the support center so that we can help you get up and running: + +https://jamkazam.desk.com + +-- Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.html.erb new file mode 100644 index 000000000..10eeeb0da --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.html.erb @@ -0,0 +1,27 @@ +<% provide(:title, @title) %> + +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+ +

We noticed that you haven’t yet connected with any friends on JamKazam. Connecting with friends is the best way to help you get into sessions with other musicians on JamKazam. Here are a couple of good ways to connect with others. +

+ +

Find Other Musicians on JamKazam
+To find and connect with other musicians who are already on JamKazam, we’d suggest you click the Musicians tile on the home screen of the app or the website: Go To Musicians Page +

+ +

This will display the JamKazam musicians sorted by latency to you - in other words, you can see which musicians have good network connections to you. Any musicians with green and yellow latency scores have good enough connections to support a play session with you. We recommend that you read the profiles of these musicians to find others with shared musical interests and good network connections to you, and then use the Message button to say hi and see if they are interested in playing with you. If they are, use the Connect button to “friend” them on JamKazam, and use the Message button to set up a time to meet online for a session. +

+ +

Invite Your Friends to Play
+One of the best ways to connect and play with others is to invite your friends from the “real world” to join you on JamKazam, and then set up a time to meet online and get into a session together. To do this, just go to www.jamkazam.com and sign in. Then move your mouse over your picture or name in the upper right corner of the screen. A menu will be displayed. Click the Invite Friends option, and then you can click Facebook, Google, or Email to easily invite your friends from different services to join you on JamKazam. +

+ +

If you have any trouble, please visit our support center at the link below any time to get help: +

+ +

https://jamkazam.desk.com +

+ +

-- Team JamKazam +

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.text.erb new file mode 100644 index 000000000..370823a10 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.text.erb @@ -0,0 +1,20 @@ +<%= @title %> + +Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- + +We noticed that you haven’t yet connected with any friends on JamKazam. Connecting with friends is the best way to help you get into sessions with other musicians on JamKazam. Here are a couple of good ways to connect with others. + +Find Other Musicians on JamKazam +To find and connect with other musicians who are already on JamKazam, we’d suggest you click the Musicians tile on the home screen of the app or the website: http://www.jamkazam.com/client#/musicians + +This will display the JamKazam musicians sorted by latency to you - in other words, you can see which musicians have good network connections to you. Any musicians with green and yellow latency scores have good enough connections to support a play session with you. We recommend that you read the profiles of these musicians to find others with shared musical interests and good network connections to you, and then use the Message button to say hi and see if they are interested in playing with you. If they are, use the Connect button to “friend” them on JamKazam, and use the Message button to set up a time to meet online for a session. + +Invite Your Friends to Play +One of the best ways to connect and play with others is to invite your friends from the “real world” to join you on JamKazam, and then set up a time to meet online and get into a session together. To do this, just go to www.jamkazam.com and sign in. Then move your mouse over your picture or name in the upper right corner of the screen. A menu will be displayed. Click the Invite Friends option, and then you can click Facebook, Google, or Email to easily invite your friends from different services to join you on JamKazam. + +If you have any trouble, please visit our support center at the link below any time to get help: + +https://jamkazam.desk.com + + +-- Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notinvite.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notinvite.html.erb new file mode 100644 index 000000000..ef9bba2bc --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notinvite.html.erb @@ -0,0 +1,16 @@ +<% provide(:title, @title) %> + +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+ +

We noticed that you have not invited any of your friends to join you to play together on JamKazam. It’s still very early in our company’s development, so we don’t have zillions of users online on our service yet. One of the best ways to connect and play with others is to invite your friends from the “real world” to join you on JamKazam, and then set up a time to meet online and get into a session together. To do this, just go to www.jamkazam.com and sign in. Then move your mouse over your picture or name in the upper right corner of the screen. A menu will be displayed. Click the Invite Friends option, and then you can click Facebook, Google, or Email to easily invite your friends from different services to join you on JamKazam. +

+ +

If you have any trouble, please visit our support center at the link below any time to get help: +

+ +

https://jamkazam.desk.com +

+ +

-- Team JamKazam +

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notinvite.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notinvite.text.erb new file mode 100644 index 000000000..372ab08da --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notinvite.text.erb @@ -0,0 +1,11 @@ +<%= @title %> + +Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- + +We noticed that you have not invited any of your friends to join you to play together on JamKazam. It’s still very early in our company’s development, so we don’t have zillions of users online on our service yet. One of the best ways to connect and play with others is to invite your friends from the “real world” to join you on JamKazam, and then set up a time to meet online and get into a session together. To do this, just go to www.jamkazam.com and sign in. Then move your mouse over your picture or name in the upper right corner of the screen. A menu will be displayed. Click the Invite Friends option, and then you can click Facebook, Google, or Email to easily invite your friends from different services to join you on JamKazam. + +If you have any trouble, please visit our support center at the link below any time to get help: + +https://jamkazam.desk.com + +-- Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.html.erb new file mode 100644 index 000000000..3b654a83a --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.html.erb @@ -0,0 +1,14 @@ +<% provide(:title, @title) %> + +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+ +

JamKazam is a young company/service built through the sweat and commitment of a small group of music-loving techies. Please help us continue to grow the service and attract more musicians to play online by liking and/or following us on Facebook, Twitter, and Google+. Just click the icons below to give us little push, thanks! +

+ +<% [:twitter, :facebook, :google].each do |site| %> + <%= link_to(image_tag("http://www.jamkazam.com/assets/content/icon_#{site}.png", :style => "vertical-align:top"), "http://www.jamkazam.com/endorse/@USERID/#{site}?src=email") %>  +<% end %> + +

-- Team JamKazam +

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.text.erb new file mode 100644 index 000000000..9dddb6ca6 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.text.erb @@ -0,0 +1,12 @@ +<%= @title %> + +Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- + +JamKazam is a young company/service built through the sweat and commitment of a small group of music-loving techies. Please help us continue to grow the service and attract more musicians to play online by liking and/or following us on Facebook, Twitter, and Google+. Just click the icons below to give us little push, thanks! + +<% [:twitter, :facebook, :google].each do |site| %> + http://www.jamkazam.com/endorse/@USERID/#{site}?src=email + +<% end %> + +-- Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/sess_notgood.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/sess_notgood.html.erb new file mode 100644 index 000000000..60ed88c69 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/sess_notgood.html.erb @@ -0,0 +1,19 @@ +<% provide(:title, @title) %> + +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+ +

We noticed that you haven’t yet rated any of your JamKazam sessions as “good”. It may be that you just are not a “rater”, and that is totally fine. But if you are not having good, high quality, productive and fun sessions, we want to help you get there! +

+ +

If you are having audio quality problems or other issues when you get into a session, please click the link below to visit our support center, and check the knowledge base articles under the Troubleshooting header to find solutions. And if that doesn’t work, please post a request for assistance in the support center so that we can help you get up and running: +

+ +

https://jamkazam.desk.com +

+ +

We really want you to be successful and have fun with this new way of playing music with others, so please reach out and let us help you! +

+ +

-- Team JamKazam +

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/sess_notgood.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/sess_notgood.text.erb new file mode 100644 index 000000000..28030d0dd --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/sess_notgood.text.erb @@ -0,0 +1,13 @@ +<%= @title %> + +Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- + +We noticed that you haven’t yet rated any of your JamKazam sessions as “good”. It may be that you just are not a “rater”, and that is totally fine. But if you are not having good, high quality, productive and fun sessions, we want to help you get there! + +If you are having audio quality problems or other issues when you get into a session, please click the link below to visit our support center, and check the knowledge base articles under the Troubleshooting header to find solutions. And if that doesn’t work, please post a request for assistance in the support center so that we can help you get up and running: + +https://jamkazam.desk.com + +We really want you to be successful and have fun with this new way of playing music with others, so please reach out and let us help you! + +-- Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/sess_notrecord.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/sess_notrecord.html.erb new file mode 100644 index 000000000..97d5f036d --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/sess_notrecord.html.erb @@ -0,0 +1,19 @@ +<% provide(:title, @title) %> + +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+ +

We noticed that you haven’t yet made a recording during any of your JamKazam sessions. Recordings are extra special on JamKazam because: +

+ + + + + +

-- Team JamKazam +

diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/sess_notrecord.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/sess_notrecord.text.erb new file mode 100644 index 000000000..301bef449 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/sess_notrecord.text.erb @@ -0,0 +1,11 @@ +<%= @title %> + +Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- + +We noticed that you haven’t yet made a recording during any of your JamKazam sessions. Recordings are extra special on JamKazam because: + +- Recordings are made both as a master mix and at the track/stem level. +- You can easily play along with your recordings when your friends aren’t available. +- You can share your recordings with family, friends and fans via Facebook, Twitter, etc. + +-- Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb index 1261901fa..c7299aa3f 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.html.erb @@ -1,4 +1,8 @@ -<% provide(:title, 'New JamKazam Musicians in your Area') %> +<% provide(:title, 'New Musicians You Should Check Out') %> +Hi <%= @user.first_name %>, + +

The following new musicians have joined JamKazam within the last week, and have Internet connections with low enough latency to you that you can have a good online session together. We'd suggest that you look through the new musicians listed below to see if any match your musical interests, and if so, click through to their profile page on the JamKazam website to send them a message or a request to connect as a JamKazam friend: +

<% link_style = "background-color:#ED3618; margin:0px 8px 0px 8px; border: solid 1px #F27861; outline: solid 2px #ED3618; padding:3px 10px; font-family:Raleway, Arial, Helvetica, sans-serif; font-size:12px; font-weight:300; cursor:pointer; color:#FC9; text-decoration:none;" %>

@@ -20,3 +24,8 @@ <% end %>

+

There are currently <%= @new_nearby.size%> musicians on JamKazam with low enough latency Internet connections to you to support a good online session. To see ALL the JamKazam musicians with whom you may want to connect and play, view our Musicians page at: http://www.jamkazam.com/client#/musicians. +

+ +

Best Regards,

+Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb index 13e391154..f1b51b9d7 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/new_musicians.text.erb @@ -1,9 +1,17 @@ New JamKazam Musicians in your Area +Hi <%= @user.first_name %>, + +The following new musicians have joined JamKazam within the last week, and have Internet connections with low enough latency to you that you can have a good online session together. We'd suggest that you look through the new musicians listed below to see if any match your musical interests, and if so, click through to their profile page on the JamKazam website to send them a message or a request to connect as a JamKazam friend: + <% @new_nearby.each do |user| %> <%= user.name %> (http://<%= @host %>/client#/profile/<%= user.id %>) <%= user.location %> <% user.instruments.collect { |inst| inst.description }.join(', ') %> <%= user.biography %> - <% end %> + +There are currently <%= @new_nearby.size%> musicians on JamKazam with low enough latency Internet connections to you to support a good online session. To see ALL the JamKazam musicians with whom you may want to connect and play, view our Musicians page at: http://www.jamkazam.com/client#/musicians. + +Best Regards, +Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb new file mode 100644 index 000000000..40788d8de --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.html.erb @@ -0,0 +1,38 @@ +<% provide(:title, @title) %> + +

Hello <%= @user.first_name %> -- +

+ +

The following new sessions that that have been posted during the last 24 hours: +

+
    +
  1. Need someone who plays an instrument that you play
  2. +
  3. Were posted by someone to whom you have either a good or medium latency connection
  4. +
+ +

Take a look through these new sessions below, and just click the RSVP button on the far right side of the row for any session in which you'd like to play. This will let the session organizer know you're interested, and you'll be notified if the session organizer accepts your request to play in that session! +

+ + + + + + + + + +<% @sessions_and_latency.each do |sess| %> + + + + + +<% end %> +
GENREDESCRIPTIONLATENCY
<%= sess.genre.description %><%= sess.description %><%= sess.latency_store %>
+ +

To see ALL the scheduled sessions that you might be interested in joining, view our Find Session page at: http://www.jamkazam.com/client#/findSession. +

+ +

Best Regards,

+ +Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb new file mode 100644 index 000000000..16ce1e7c1 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_daily.text.erb @@ -0,0 +1,21 @@ +<% provide(:title, @title) %> + +Hello <%= @user.first_name %> -- + +The following new sessions that that have been posted during the last 24 hours: + +1. Need someone who plays an instrument that you play +2. Were posted by someone to whom you have either a good or medium latency connection + +Take a look through these new sessions below, and just click the RSVP button on the far right side of the row for any session in which you'd like to play. This will let the session organizer know you're interested, and you'll be notified if the session organizer accepts your request to play in that session! + +GENRE | DESCRIPTION | LATENCY +<% @sessions_and_latency.each do |sess| %> +<%= sess.genre.description %> | <%= sess.description %> | <%= sess.latency_store %> +<% end %> + +To see ALL the scheduled sessions that you might be interested in joining, view our Find Session page at: http://www.jamkazam.com/client#/findSession. + +Best Regards, + +Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb index 96fea047f..b74876e92 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.html.erb @@ -1,27 +1,45 @@ <% provide(:title, 'Welcome to JamKazam!') %> +

Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- +

+

We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are links to some resources that can help to get you up and running quickly.

-Tutorial videos that show you how to use the key features of the product:
+Getting Started Video
+We recommend watching this video before you jump into the service just to get oriented. It will really help you hit the ground running: +https://www.youtube.com/watch?v=VexH4834o9I +

+ +

+Other Great Tutorial Videos
+There are several other very great videos that will help you understand how to find and connect with other musicians on the service, create your own sessions or find and join other musicians’ sessions, play in sessions, record and share your performances, and even live broadcast your sessions to family, friends, and fans. Check these helpful videos out here: https://jamkazam.desk.com/customer/portal/articles/1304097-tutorial-videos

-Getting Started knowledge base articles:
+Knowledge Base Articles
+You can find Getting Started knowledge base articles on things like frequently asked questions (FAQ), minimum system requirements for your Windows or Mac computer, how to troubleshoot audio problems in sessions, and more here: https://jamkazam.desk.com/customer/portal/topics/564807-getting-started/articles

-Support Portal in case you run into trouble and need help:
+JamKazam Support Portal
+If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to get you up and running. You can find our support portal here: https://jamkazam.desk.com/

- We look forward to seeing - and hearing - you online soon! +JamKazam Community Forum
+And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our community forum here: +http://forums.jamkazam.com/

-  - Team JamKazam \ No newline at end of file +

+Please take a moment to like or follow us by clicking the icons below, and we look forward to seeing – and hearing – you online soon! +

+ +  -- Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb index 89f7d39fc..ef28cd06c 100644 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/welcome_message.text.erb @@ -1,16 +1,27 @@ -We're delighted that you have decided to try the JamKazam service, -and we hope that you will enjoy using JamKazam to play music with others. -Following are links to some resources that can help to get you up and running quickly. +Hello <%= EmailBatchProgression::VAR_FIRST_NAME %> -- -Tutorial videos that show you how to use the key features of the product: +We're delighted that you have decided to try the JamKazam service, and we hope that you will enjoy using JamKazam to play music with others. Following are links to some resources that can help to get you up and running quickly. + +Getting Started Video +We recommend watching this video before you jump into the service just to get oriented. It will really help you hit the ground running: +https://www.youtube.com/watch?v=VexH4834o9I + +Other Great Tutorial Videos +There are several other very great videos that will help you understand how to find and connect with other musicians on the service, create your own sessions or find and join other musicians’ sessions, play in sessions, record and share your performances, and even live broadcast your sessions to family, friends, and fans. Check these helpful videos out here: https://jamkazam.desk.com/customer/portal/articles/1304097-tutorial-videos -Getting Started knowledge base articles: +Knowledge Base Articles +You can find Getting Started knowledge base articles on things like frequently asked questions (FAQ), minimum system requirements for your Windows or Mac computer, how to troubleshoot audio problems in sessions, and more here: https://jamkazam.desk.com/customer/portal/topics/564807-getting-started/articles -Support Portal in case you run into trouble and need help: -https://jamkazam.desk.com/ +JamKazam Support Portal +If you run into trouble and need help, please reach out to us. We will be glad to do everything we can to get you up and running. You can find our support portal here: +https://jamkazam.desk.com -We look forward to seeing - and hearing - you online soon! +JamKazam Community Forum +And if you just want to chat, share tips and war stories, and hang out with fellow JamKazamers, you can visit our community forum here: +http://forums.jamkazam.com - - Team JamKazam \ No newline at end of file +Please take a moment to like or follow us by clicking the icons below, and we look forward to seeing – and hearing – you online soon! + +-- Team JamKazam diff --git a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb index 159221f5b..d12abf802 100644 --- a/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb +++ b/ruby/lib/jam_ruby/app/views/layouts/user_mailer.html.erb @@ -49,8 +49,8 @@ diff --git a/ruby/lib/jam_ruby/connection_manager.rb b/ruby/lib/jam_ruby/connection_manager.rb index 589841943..efd6dd12b 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -153,11 +153,11 @@ SQL # NOTE this is only used for testing purposes; # actual deletes will be processed in the websocket context which cleans up dependencies def expire_stale_connections() - self.stale_connection_client_ids().each { |client| self.delete_connection(client[:client_id]) } + self.stale_connection_client_ids.each { |client| self.delete_connection(client[:client_id]) } end # expiring connections in stale state, which deletes them - def stale_connection_client_ids() + def stale_connection_client_ids clients = [] ConnectionManager.active_record_transaction do |connection_manager| conn = connection_manager.pg_conn @@ -347,7 +347,7 @@ SQL connection = Connection.find_by_client_id_and_user_id!(client_id, user.id) - connection.join_the_session(music_session, as_musician, tracks) + connection.join_the_session(music_session, as_musician, tracks, user) # connection.music_session_id = music_session.id # connection.as_musician = as_musician # connection.joining_session = true diff --git a/ruby/lib/jam_ruby/constants/validation_messages.rb b/ruby/lib/jam_ruby/constants/validation_messages.rb index 2a13a87b7..20edf028d 100644 --- a/ruby/lib/jam_ruby/constants/validation_messages.rb +++ b/ruby/lib/jam_ruby/constants/validation_messages.rb @@ -41,7 +41,7 @@ module ValidationMessages INVALID_FPFILE = "is not valid" #connection - + USER_OR_LATENCY_TESTER_PRESENT = "user or latency_tester must be present" SELECT_AT_LEAST_ONE = "Please select at least one track" # DO NOT CHANGE THIS TEXT MESSAGE UNLESS YOU CHANGE createSession.js.erb, which is looking for it FAN_CAN_NOT_JOIN_AS_MUSICIAN = "A fan can not join a music session as a musician" MUSIC_SESSION_MUST_BE_SPECIFIED = "A music session must be specified" diff --git a/ruby/lib/jam_ruby/message_factory.rb b/ruby/lib/jam_ruby/message_factory.rb index 626b5c776..28bb2d13b 100644 --- a/ruby/lib/jam_ruby/message_factory.rb +++ b/ruby/lib/jam_ruby/message_factory.rb @@ -22,13 +22,11 @@ module JamRuby Jampb::ClientMessage.parse(payload) end - # create a login message using user/pass - def login_with_user_pass(username, password, options = {}) + # create a login message using client_id (used by latency_tester) + def login_with_client_id(client_id) login = Jampb::Login.new( - :username => username, - :password => password, - :client_id => options[:client_id], - :client_type => options[:client_type] + :client_id => client_id, + :client_type => Connection::TYPE_LATENCY_TESTER ) Jampb::ClientMessage.new( @@ -38,6 +36,22 @@ module JamRuby ) end + # create a login message using user/pass + def login_with_user_pass(username, password, options = {}) + login = Jampb::Login.new( + :username => username, + :password => password, + :client_id => options[:client_id], + :client_type => options[:client_type] + ) + + Jampb::ClientMessage.new( + :type => ClientMessage::Type::LOGIN, + :route_to => SERVER_TARGET, + :login => login + ) + end + # create a login message using token (a cookie or similar) def login_with_token(token, options = {}) login = Jampb::Login.new( diff --git a/ruby/lib/jam_ruby/models/chat_message.rb b/ruby/lib/jam_ruby/models/chat_message.rb index c02ed1d7b..028e2d411 100644 --- a/ruby/lib/jam_ruby/models/chat_message.rb +++ b/ruby/lib/jam_ruby/models/chat_message.rb @@ -27,7 +27,10 @@ module JamRuby start = params[:start].presence start = start.to_i || 0 - query = ChatMessage.offset(start).limit(limit) + music_session_id = params[:music_session] + + query = ChatMessage.where('music_session_id = ?', music_session_id) + .offset(start).limit(limit) if query.length == 0 [query, nil] diff --git a/ruby/lib/jam_ruby/models/connection.rb b/ruby/lib/jam_ruby/models/connection.rb index f5e2d315a..9def6a7c1 100644 --- a/ruby/lib/jam_ruby/models/connection.rb +++ b/ruby/lib/jam_ruby/models/connection.rb @@ -6,6 +6,7 @@ module JamRuby # client_types TYPE_CLIENT = 'client' TYPE_BROWSER = 'browser' + TYPE_LATENCY_TESTER = 'latency_tester' attr_accessor :joining_session @@ -13,11 +14,14 @@ module JamRuby belongs_to :user, :class_name => "JamRuby::User" belongs_to :music_session, :class_name => "JamRuby::ActiveMusicSession", foreign_key: :music_session_id + has_one :latency_tester, class_name: 'JamRuby::LatencyTester', foreign_key: :client_id, primary_key: :client_id has_many :tracks, :class_name => "JamRuby::Track", :inverse_of => :connection, :foreign_key => 'connection_id', :dependent => :delete_all validates :as_musician, :inclusion => {:in => [true, false]} - validates :client_type, :inclusion => {:in => [TYPE_CLIENT, TYPE_BROWSER]} + validates :client_type, :inclusion => {:in => [TYPE_CLIENT, TYPE_BROWSER, TYPE_LATENCY_TESTER]} validate :can_join_music_session, :if => :joining_session? + validate :user_or_latency_tester_present + after_save :require_at_least_one_track_when_in_session, :if => :joining_session? after_create :did_create after_save :report_add_participant @@ -134,7 +138,7 @@ module JamRuby end def did_create - self.user.update_lat_lng(self.ip_address) if self.user && self.ip_address + # self.user.update_lat_lng(self.ip_address) if self.user && self.ip_address end def report_add_participant @@ -148,13 +152,18 @@ module JamRuby true end - def join_the_session(music_session, as_musician, tracks) + def join_the_session(music_session, as_musician, tracks, user) self.music_session_id = music_session.id self.as_musician = as_musician self.joining_session = true self.joined_session_at = Time.now associate_tracks(tracks) unless tracks.nil? self.save + + # if user joins the session as a musician, update their addr and location + if as_musician + user.update_addr_loc(self, 'j') + end end def associate_tracks(tracks) @@ -185,5 +194,12 @@ module JamRuby end end + def user_or_latency_tester_present + if user.nil? && client_type != TYPE_LATENCY_TESTER + puts client_type + errors.add(:connection, ValidationMessages::USER_OR_LATENCY_TESTER_PRESENT) + end + end + end end diff --git a/ruby/lib/jam_ruby/models/diagnostic.rb b/ruby/lib/jam_ruby/models/diagnostic.rb index a4be2dab5..1bb12ac90 100644 --- a/ruby/lib/jam_ruby/models/diagnostic.rb +++ b/ruby/lib/jam_ruby/models/diagnostic.rb @@ -17,6 +17,9 @@ module JamRuby # this implies a coding error MISSING_CLIENT_STATE = 'MISSING_CLIENT_STATE' + # the underlying database connection is gone when the heartbeat comes in + MISSING_CONNECTION = 'MISSING_CONNECTION' + # websocket gateway did not recognize message. indicates out-of-date websocket-gateway UNKNOWN_MESSAGE_TYPE = 'UNKNOWN_MESSAGE_TYPE' @@ -26,9 +29,15 @@ module JamRuby # websocket gateway got a client with the same client_id as an already-connected client DUPLICATE_CLIENT = 'DUPLICATE_CLIENT' + # info about how the test went + NETWORK_TEST_RESULT = 'NETWORK_TEST_RESULT' + + # step 2 of the FTUE... could the user select their gear? + GEAR_SELECTION = 'GEAR_SELECTION' + DIAGNOSTIC_TYPES = [NO_HEARTBEAT_ACK, WEBSOCKET_CLOSED_REMOTELY, EXPIRED_STALE_CONNECTION, MISSING_CLIENT_STATE, UNKNOWN_MESSAGE_TYPE, MISSING_ROUTE_TO, - DUPLICATE_CLIENT, WEBSOCKET_CLOSED_LOCALLY] + DUPLICATE_CLIENT, WEBSOCKET_CLOSED_LOCALLY, NETWORK_TEST_RESULT, GEAR_SELECTION] # creator types # CLIENT = 'client' diff --git a/ruby/lib/jam_ruby/models/email_batch.rb b/ruby/lib/jam_ruby/models/email_batch.rb index 02c729dda..8389c56c5 100644 --- a/ruby/lib/jam_ruby/models/email_batch.rb +++ b/ruby/lib/jam_ruby/models/email_batch.rb @@ -13,7 +13,7 @@ module JamRuby VAR_LAST_NAME = '@LASTNAME' DEFAULT_SENDER = "noreply@jamkazam.com" - BATCH_SIZE = 5 + BATCH_SIZE = 500 BODY_TEMPLATE =< '#{time_since_last_batch(SINCE_DAYS)}' +SQL + ActiveRecord::Base.connection.execute(sql) + end + + def _fetch_receiver_candidates + ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS #{TMP_CAND}") + sql =< { + :subject => "Get the free JamKazam app now and start playing with others!", + :title => "Download the Free JamKazam App" + }, + :client_dl_notrun => { + :subject => "Having trouble running the JamKazam application?", + :title => "Running the JamKazam App" + }, + :client_run_notgear => { + :subject => "Having trouble setting up your audio gear for JamKazam?", + :title => "Setting Up and Qualifying Your Audio Gear on JamKazam" + }, + :gear_notsess => { + :subject => "Having trouble getting into a session with other musicians?", + :title => "Tips on Getting into Sessions with Other Musicians" + }, + :sess_notgood => { + :subject => "Have you played in a “good” session on JamKazam yet?", + :title => "Having a Good Session on JamKazam" + }, + :reg_notinvite => { + :subject => "Invite your friends to JamKazam, best way to play online!", + :title => "Invite Your Friends to Come and Play with You" + }, + :reg_notconnect => { + :subject => "Make friends on JamKazam and play more music!", + :title => "Connecting with Friends on JamKazam" + }, + :reg_notlike => { + :subject => "Please give us a LIKE!", + :title => "Like/Follow JamKazam" + }, + :sess_notrecord => { + :subject => "Want to make recordings during your JamKazam sessions?", + :title => "Making & Sharing Recordings on JamKazam" + }, + } + + def self.subject(subtype=nil) + SUBTYPE_METADATA[subtype][:subject] if subtype + end + + def self.title(subtype=nil) + SUBTYPE_METADATA[subtype][:title] if subtype + end + + def self.subtype_trigger_interval(subtype) + [:reg_notlike, :sess_notrecord].include?(subtype) ? [7,14,21] : [2,5,14] + end + + def self.days_past_for_trigger_index(subtype, idx) + self.subtype_trigger_interval(subtype)[idx] + end + + def days_diff(tidx) + self.class.days_past_for_trigger_index(self.sub_type,tidx) - self.class.days_past_for_trigger_index(self.sub_type, tidx - 1) + end + + def days_past_for_trigger_index(idx) + self.class.subtype_trigger_interval(self.sub_type)[idx] + end + + def make_set(uu, trigger_idx) + EmailBatchSet.progress_set(self, uu, trigger_idx) + end + + def trigger_date_constraint(trigger_idx, tbl='tt') + intervals = self.class.subtype_trigger_interval(self.sub_type) + date_constraint = (Time.now - intervals[trigger_idx].days + 1.hour).strftime('%Y-%m-%d %H:%M:%S') + + case self.sub_type.to_sym + when :client_notdl, :reg_notconnect, :reg_notinvite, :reg_notlike + return "#{tbl}.created_at < '#{date_constraint}'" + when :client_dl_notrun + return "#{tbl}.first_downloaded_client_at < '#{date_constraint}'" + when :client_run_notgear + return "#{tbl}.first_ran_client_at < '#{date_constraint}'" + when :gear_notsess + return "#{tbl}.first_certified_gear_at < '#{date_constraint}'" + when :sess_notgood + return "#{tbl}.first_real_music_session_at < '#{date_constraint}'" + when :sess_notrecord + return "#{tbl}.first_real_music_session_at < '#{date_constraint}'" + end + end + + def progress_column_constraint(tbl='tt') + case self.sub_type.to_sym + when :client_notdl + return "#{tbl}.first_downloaded_client_at IS NULL" + when :client_dl_notrun + return "#{tbl}.first_downloaded_client_at IS NOT NULL AND #{tbl}.first_ran_client_at IS NULL" + when :client_run_notgear + return "#{tbl}.first_ran_client_at IS NOT NULL AND #{tbl}.first_certified_gear_at IS NULL" + when :gear_notsess + return "#{tbl}.first_certified_gear_at IS NOT NULL AND #{tbl}.first_real_music_session_at IS NULL" + when :sess_notgood + return "#{tbl}.first_real_music_session_at IS NOT NULL AND #{tbl}.first_good_music_session_at IS NULL" + when :sess_notrecord + return "#{tbl}.first_real_music_session_at IS NOT NULL AND #{tbl}.first_recording_at IS NULL" + when :reg_notinvite + return "#{tbl}.first_invited_at IS NULL" + when :reg_notconnect + return "#{tbl}.first_friended_at IS NULL" + when :reg_notlike + return "#{tbl}.first_social_promoted_at IS NULL" + end + '' + end + + def fetch_recipients(trigger_idx=0, per_page=BATCH_SIZE) + fetched = [] + offset = 0 + if 0==trigger_idx + prev_date_sql = 'tt.prev_trigger_date IS NULL' + else + prev_date_sql = "tt.prev_trigger_date + interval '#{self.days_diff(trigger_idx)} days' <= '#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}'" + end + countsql =< '#{time_since_last_batch(SINCE_DAYS)}' AND + scheduled_start >= '#{Time.now() + MIN_HOURS_START.hours}' AND + (rrrs.rsvp_slot_id IS NULL OR rrrs.chosen != 't') +SQL + ActiveRecord::Base.connection.execute(sql) + end + + def _collect_eligible_recipients + ActiveRecord::Base.connection.execute("DROP TABLE IF EXISTS #{TMP_USER}") + # load eligible recipients into tmp table + sql =< 'JamRuby::EmailBatch' + belongs_to :user, :class_name => 'JamRuby::User' - def self.deliver_set(batch_id, user_ids) + # def self.load_set(batch, user_ids) + # bset = self.new + # bset.email_batch_id = batch.id + # bset.user_ids = user_ids.join(',') + # bset.started_at = Time.now + # bset.batch_count = user_ids.size + # bset.save! + # bset + # end + + def self.sent_email(batch, user_id) bset = self.new - bset.email_batch_id = batch_id - bset.user_ids = user_ids.join(',') + bset.email_batch_id = batch.id + bset.user_id = user_id bset.started_at = Time.now - bset.batch_count = user_ids.size + bset.batch_count = 1 bset.save! - - if 'test' == Rails.env - BatchMailer.send_batch_email(bset.email_batch_id, user_ids).deliver! - else - BatchMailer.send_batch_email(bset.email_batch_id, user_ids).deliver - end bset end + + def self.progress_set(batch, user, trigger_idx) + bset = self.new + bset.email_batch = batch + bset.user = user + bset.started_at = Time.now + bset.batch_count = 1 + bset.trigger_index = trigger_idx + bset.sub_type = batch.sub_type + bset.save! + bset + end + + def self.scheduled_session_set(batch, receiver, sessions) + bset = self.new + bset.email_batch = batch + bset.user = receiver + bset.user_ids = sessions.map(&:id).join(',') + bset.started_at = Time.now + bset.batch_count = 1 + bset.save! + bset + end + + def self.new_musician_set(batch, receiver, new_musicians) + bset = self.new + bset.email_batch = batch + bset.user = receiver + bset.user_ids = new_musicians.map(&:id).join(',') + bset.started_at = Time.now + bset.batch_count = 1 + bset.save! + bset + end + + def subject + unless sub_type.blank? + return EmailBatchProgression.subject(self.sub_type.to_sym) + end + '' + end + + def title + unless sub_type.blank? + return EmailBatchProgression.title(self.sub_type.to_sym) + end + '' + end + + def previous_trigger_date + return nil if 0 == self.trigger_index.to_i || self.user_id.nil? + self.class + .where(['email_batch__id = ? AND user_id = ? AND sub_type = ? AND trigger_index = ?', + self.email_batch_id, self.user_id, self.sub_type, self.trigger_index - 1]) + .pluck(:created_at) + .limit(1) + .first + end + end end diff --git a/ruby/lib/jam_ruby/models/email_error.rb b/ruby/lib/jam_ruby/models/email_error.rb deleted file mode 100644 index fe693b849..000000000 --- a/ruby/lib/jam_ruby/models/email_error.rb +++ /dev/null @@ -1,80 +0,0 @@ -module JamRuby - class EmailError < ActiveRecord::Base - self.table_name = "email_errors" - - belongs_to :user, :class_name => 'JamRuby::User' - - default_scope :order => 'email_date DESC' - - ERR_BOUNCE = :bounce - ERR_INVALID = :invalid - - SENDGRID_UNAME = 'jamkazam' - SENDGRID_PASSWD = 'jamjamblueberryjam' - - def self.sendgrid_url(resource, action='get', params='') - start_date, end_date = self.date_range - "https://api.sendgrid.com/api/#{resource}.#{action}.json?api_user=#{EmailError::SENDGRID_UNAME}&api_key=#{EmailError::SENDGRID_PASSWD}&date=1&start_date=#{start_date.strftime('%Y-%m-%d')}&end_date=#{end_date.strftime('%Y-%m-%d')}&#{params}" - end - - def self.date_range - tt = Time.now - if eerr = self.first - return [eerr.email_date, tt] - end - [tt - 1.year, tt] - end - - def self.did_capture?(email_addy) - self.where(:email_address => email_addy).limit(1).first.present? - end - - def self.bounce_errors - uu = self.sendgrid_url('bounces') - response = RestClient.get(uu) - if 200 == response.code - return JSON.parse(response.body).collect do |jj| - next if self.did_capture?(jj['email']) - - ee = EmailError.new - ee.error_type = 'bounces' - ee.email_address = jj['email'] - ee.user_id = User.where(:email => ee.email_address).pluck(:id).first - ee.status = jj['status'] - ee.email_date = jj['created'] - ee.reason = jj['reason'] - ee.save! - # RestClient.delete(self.sendgrid_url('bounces', 'delete', "email=#{ee.email_address}")) - ee - end - end - end - - def self.invalid_errors - uu = self.sendgrid_url('invalidemails') - response = RestClient.get(uu) - if 200 == response.code - return JSON.parse(response.body).collect do |jj| - next if self.did_capture?(jj['email']) - - ee = EmailError.new - ee.error_type = 'invalidemails' - ee.email_address = jj['email'] - ee.user_id = User.where(:email => ee.email_address).pluck(:id).first - ee.email_date = jj['created'] - ee.reason = jj['reason'] - ee.save! - uu = - # RestClient.delete(self.sendgrid_url('invalidemails', 'delete', "email=#{ee.email_address}")) - ee - end - end - end - - def self.capture_errors - EmailError.bounce_errors - EmailError.invalid_errors - end - - end -end diff --git a/ruby/lib/jam_ruby/models/get_work.rb b/ruby/lib/jam_ruby/models/get_work.rb index f82ca00f4..2d52615c8 100644 --- a/ruby/lib/jam_ruby/models/get_work.rb +++ b/ruby/lib/jam_ruby/models/get_work.rb @@ -3,15 +3,15 @@ module JamRuby self.table_name = "connections" - def self.get_work(mylocidispid) - list = self.get_work_list(mylocidispid) + def self.get_work(mylocidispid, myaddr) + list = self.get_work_list(mylocidispid, myaddr) return nil if list.nil? return nil if list.length == 0 return list[0] end - def self.get_work_list(mylocidispid) - r = GetWork.select(:client_id).find_by_sql("select get_work(#{mylocidispid}) as client_id") + def self.get_work_list(mylocidispid, myaddr) + r = GetWork.select(:client_id).find_by_sql("select get_work(#{mylocidispid}, #{myaddr}) as client_id") #puts("r = #{r}") a = r.map {|i| i.client_id} #puts("a = #{a}") diff --git a/ruby/lib/jam_ruby/models/latency_tester.rb b/ruby/lib/jam_ruby/models/latency_tester.rb new file mode 100644 index 000000000..2bc1071ae --- /dev/null +++ b/ruby/lib/jam_ruby/models/latency_tester.rb @@ -0,0 +1,79 @@ +module JamRuby + class LatencyTester < ActiveRecord::Base + + belongs_to :connection, class_name: 'JamRuby::Connection', foreign_key: :client_id, primary_key: :client_id + + def heartbeat_interval_client + nil + end + + def connection_expire_time_client + nil + end + + def self.select_latency_tester + LatencyTester.joins(:connection).first! + end + + # we need to find that latency_tester with the specified connection (and reconnect it) + # or bootstrap a new latency_tester + def self.connect(options) + client_id = options[:client_id] + ip_address = options[:ip_address] + connection_stale_time = options[:connection_stale_time] + connection_expire_time = options[:connection_expire_time] + # first try to find a LatencyTester with that client_id + latency_tester = LatencyTester.find_by_client_id(client_id) + + if latency_tester + if latency_tester.connection + connection = latency_tester.connection + else + connection = Connection.new + connection.client_id = client_id + latency_tester.connection = connection + end + else + latency_tester = LatencyTester.new + latency_tester.client_id = client_id + unless latency_tester.save + return latency_tester + end + connection = Connection.new + connection.latency_tester = latency_tester + connection.client_id = client_id + end + + if ip_address and !ip_address.eql?(connection.ip_address) + # locidispid stuff + addr = JamIsp.ip_to_num(ip_address) + isp = JamIsp.lookup(addr) + if isp.nil? then ispid = 0 else ispid = isp.coid end + block = GeoIpBlocks.lookup(addr) + if block.nil? then locid = 0 else locid = block.locid end + location = GeoIpLocations.lookup(locid) + if location.nil? + # todo what's a better default location? + locidispid = 0 + else + locidispid = locid*1000000+ispid + end + + connection.ip_address = ip_address + connection.addr = addr + connection.locidispid = locidispid + end + + connection.client_type = 'latency_tester' + connection.aasm_state = Connection::CONNECT_STATE.to_s + connection.stale_time = connection_stale_time + connection.expire_time = connection_expire_time + connection.as_musician = false + unless connection.save + return connection + end + + return latency_tester + end + end +end diff --git a/ruby/lib/jam_ruby/models/max_mind_geo.rb b/ruby/lib/jam_ruby/models/max_mind_geo.rb index f11e3ddf2..53a9c9716 100644 --- a/ruby/lib/jam_ruby/models/max_mind_geo.rb +++ b/ruby/lib/jam_ruby/models/max_mind_geo.rb @@ -88,35 +88,42 @@ module JamRuby end end end - User.find_each { |usr| usr.update_lat_lng } + # User.find_each { |usr| usr.update_lat_lng } Band.find_each { |bnd| bnd.update_lat_lng } end def self.where_latlng(relation, params, current_user=nil) - if 0 < (distance = params[:distance].to_i) - latlng = [] - if location_city = params[:city] - if geo = self.where(:city => params[:city]).limit(1).first + # this is only valid to call when relation is about bands + distance = params[:distance].to_i + if distance > 0 + latlng = nil + location_city = params[:city] + location_state = params[:state] + location_country = params[:country] + remote_ip = params[:remote_ip] + + if location_city and location_state and location_country + geo = self.where(city: location_city, region: location_state, country: location_country).limit(1).first + if geo and geo.lat and geo.lng and (geo.lat != 0 or geo.lng != 0) + # it isn't reasonable for both to be 0... latlng = [geo.lat, geo.lng] end - elsif current_user - if current_user.lat.nil? || current_user.lng.nil? - if params[:remote_ip] && (geo = self.ip_lookup(params[:remote_ip])) - geo.lat = nil if geo.lat = 0 - geo.lng = nil if geo.lng = 0 - latlng = [geo.lat, geo.lng] if geo.lat && geo.lng - end - else - latlng = [current_user.lat, current_user.lng] + elsif current_user and current_user.locidispid and current_user.locidispid != 0 + location = GeoIpLocations.lookup(current_user.locidispid/1000000) + if location and location.latitude and location.longitude and (location.latitude != 0 or location.longitude != 0) + # it isn't reasonable for both to be 0... + latlng = [location.latitude, location.longitude] + end + elsif remote_ip + geo = self.ip_lookup(remote_ip) + if geo and geo.lat and geo.lng and (geo.lat != 0 or geo.lng != 0) + # it isn't reasonable for both to be 0... + latlng = [geo.lat, geo.lng] end - elsif params[:remote_ip] && (geo = self.ip_lookup(params[:remote_ip])) - geo.lat = nil if geo.lat = 0 - geo.lng = nil if geo.lng = 0 - latlng = [geo.lat, geo.lng] if geo.lat && geo.lng end - if latlng.present? - relation = relation.where(['lat IS NOT NULL AND lng IS NOT NULL']) - .within(distance, :origin => latlng) + + if latlng + relation = relation.where(['lat IS NOT NULL AND lng IS NOT NULL']).within(distance, origin: latlng) end end relation diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index bacf999c2..67489f9bc 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -10,6 +10,9 @@ module JamRuby attr_accessor :legal_terms, :recurring_mode, :language_description, :scheduled_start_time, :access_description + # used for temporary data store of latency between creator and some other user + attr_accessor :latency_store + self.table_name = "music_sessions" self.primary_key = 'id' @@ -294,7 +297,17 @@ module JamRuby end def language_description - ISO_639.find_by_code(self.language).english_name + if self.language.blank? + self.language = "en" + end + + iso639Details = ISO_639.find_by_code(self.language) + + unless iso639Details.blank? + return iso639Details.english_name + else + return "English" + end end def scheduled_start_time diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index 620d66887..0d3266d40 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -712,7 +712,7 @@ module JamRuby @@mq_router.publish_to_user(target_user.id, msg) begin - UserMailer.send_scheduled_session_rsvp_cancelled(target_user.email, notification_msg, music_session).deliver + UserMailer.scheduled_session_rsvp_cancelled(target_user.email, notification_msg, music_session).deliver rescue => e @@log.error("Unable to send send_scheduled_session_rsvp_cancelled email to offline user #{target_user.email} #{e}") end diff --git a/ruby/lib/jam_ruby/models/region.rb b/ruby/lib/jam_ruby/models/region.rb index decc05b9a..8a44a4f6d 100644 --- a/ruby/lib/jam_ruby/models/region.rb +++ b/ruby/lib/jam_ruby/models/region.rb @@ -7,47 +7,50 @@ module JamRuby self.where(countrycode: country).order('regionname asc').all end - def self.import_from_xx_region(countrycode, file) + def self.import_from_region_codes(file) - # File xx_region.csv + # File region_codes.csv # Format: - # region,regionname + # countrycode,region,regionname - # what this does is not replace the contents of the table, but rather update the specifies rows with the names. - # any rows not specified are left alone. the parameter countrycode denote the country of the region (when uppercased) - - raise "countrycode (#{MaxMindIsp.quote_value(countrycode)}) is missing or invalid (it must be two characters)" unless countrycode and countrycode.length == 2 - countrycode = countrycode.upcase + # what this does is replace the contents of the table with the new data. self.transaction do - self.connection.execute "update #{self.table_name} set regionname = region where countrycode = #{MaxMindIsp.quote_value(countrycode)}" + self.connection.execute "delete from #{self.table_name}" File.open(file, 'r:ISO-8859-1') do |io| - saved_level = ActiveRecord::Base.logger ? ActiveRecord::Base.logger.level : 0 + saved_level = ActiveRecord::Base.logger ? ActiveRecord::Base.logger.level : -1 count = 0 - - ncols = 2 + errors = 0 + ncols = 3 csv = ::CSV.new(io, {encoding: 'ISO-8859-1', headers: false}) csv.each do |row| raise "file does not have expected number of columns (#{ncols}): #{row.length}" unless row.length == ncols - region = row[0] - regionname = row[1] + countrycode = row[0] + region = row[1] + regionname = row[2] - stmt = "UPDATE #{self.table_name} SET regionname = #{MaxMindIsp.quote_value(regionname)} WHERE countrycode = #{MaxMindIsp.quote_value(countrycode)} AND region = #{MaxMindIsp.quote_value(region)}" - self.connection.execute stmt - count += 1 + if countrycode.length == 2 and region.length == 2 and regionname.length >= 2 and regionname.length <= 64 - if ActiveRecord::Base.logger and ActiveRecord::Base.logger.level < Logger::INFO - ActiveRecord::Base.logger.debug "... logging updates to #{self.table_name} suspended ..." - ActiveRecord::Base.logger.level = Logger::INFO + stmt = "INSERT INTO #{self.table_name} (countrycode, region, regionname) VALUES (#{self.connection.quote(countrycode)}, #{self.connection.quote(region)}, #{self.connection.quote(regionname)})" + self.connection.execute stmt + count += 1 + + if ActiveRecord::Base.logger and ActiveRecord::Base.logger.level < Logger::INFO + ActiveRecord::Base.logger.debug "... logging updates to #{self.table_name} suspended ..." + ActiveRecord::Base.logger.level = Logger::INFO + end + else + ActiveRecord::Base.logger.warn("bogus region_codes record '#{countrycode}', '#{region}', '#{regionname}'") if ActiveRecord::Base.logger + errors += 1 end end if ActiveRecord::Base.logger ActiveRecord::Base.logger.level = saved_level - ActiveRecord::Base.logger.debug "updated #{count} records in #{self.table_name}" + ActiveRecord::Base.logger.debug "inserted #{count} records into #{self.table_name}, #{errors} errors" end end # file end # transaction diff --git a/ruby/lib/jam_ruby/models/rsvp_request.rb b/ruby/lib/jam_ruby/models/rsvp_request.rb index 9c1f9da48..ed8ea3736 100644 --- a/ruby/lib/jam_ruby/models/rsvp_request.rb +++ b/ruby/lib/jam_ruby/models/rsvp_request.rb @@ -42,8 +42,8 @@ module JamRuby # verify invitation exists for this user and session invitation = Invitation.where("music_session_id = ? AND receiver_id = ?", music_session.id, user.id) - if invitation.blank? - raise PermissionError, "Only a session invitee can create an RSVP." + if invitation.blank? && !music_session.open_rsvps + raise PermissionError, "Only a session invitee can create an RSVP for this session." end # verify slot IDs exist in request @@ -56,6 +56,7 @@ module JamRuby @rsvp.user = user slot_ids = params[:rsvp_slots] + instruments = [] # for each slot requested, do the following: @@ -204,9 +205,9 @@ module JamRuby rsvp_request.canceled = true rsvp_request.cancel_all = false - when 'no' - rsvp_request.canceled = false - rsvp_request.cancel_all = false + # when 'no' + # rsvp_request.canceled = false + # rsvp_request.cancel_all = false when 'all' rsvp_request.canceled = true diff --git a/ruby/lib/jam_ruby/models/score.rb b/ruby/lib/jam_ruby/models/score.rb index 570d409f4..d1849c7b8 100644 --- a/ruby/lib/jam_ruby/models/score.rb +++ b/ruby/lib/jam_ruby/models/score.rb @@ -3,15 +3,19 @@ require 'ipaddr' module JamRuby class Score < ActiveRecord::Base + MAX_YELLOW_LATENCY = 40 + self.table_name = 'scores' - attr_accessible :alocidispid, :anodeid, :aaddr, :blocidispid, :bnodeid, :baddr, :score, :score_dt, :scorer + attr_accessible :alocidispid, :anodeid, :aaddr, :blocidispid, :bnodeid, :baddr, :score, :score_dt, :scorer, :scoring_data default_scope order('score_dt desc') - def self.createx(alocidispid, anodeid, aaddr, blocidispid, bnodeid, baddr, score, score_dt) + def self.createx(alocidispid, anodeid, aaddr, blocidispid, bnodeid, baddr, score, score_dt=nil, score_data=nil) score_dt = Time.new.utc if score_dt.nil? - Score.create(alocidispid: alocidispid, anodeid: anodeid, aaddr: aaddr, blocidispid: blocidispid, bnodeid: bnodeid, baddr: baddr, score: score, scorer: 0, score_dt: score_dt) + score = score.ceil + raise "score must be positive" if score <= 0 + Score.create(alocidispid: alocidispid, anodeid: anodeid, aaddr: aaddr, blocidispid: blocidispid, bnodeid: bnodeid, baddr: baddr, score: score, scorer: 0, score_dt: score_dt, scoring_data: score_data) Score.create(alocidispid: blocidispid, anodeid: bnodeid, aaddr: baddr, blocidispid: alocidispid, bnodeid: anodeid, baddr: aaddr, score: score, scorer: 1, score_dt: score_dt) if alocidispid != blocidispid end @@ -25,5 +29,10 @@ module JamRuby return -1 if s.nil? return s.score end + + def self.score_conns(c1, c2, score) + self.createx(c1.locidispid, c1.client_id, c1.addr, c2.locidispid, c2.client_id, c2.addr, score) + end + end end diff --git a/ruby/lib/jam_ruby/models/search.rb b/ruby/lib/jam_ruby/models/search.rb index 77509d8ad..acb82fdc0 100644 --- a/ruby/lib/jam_ruby/models/search.rb +++ b/ruby/lib/jam_ruby/models/search.rb @@ -103,6 +103,18 @@ module JamRuby DISTANCE_OPTS = B_DISTANCE_OPTS = M_DISTANCE_OPTS = [['Any', 0], [1000.to_s, 1000], [500.to_s, 500], [250.to_s, 250], [100.to_s, 100], [50.to_s, 50], [25.to_s, 25]] + # the values for score ranges are raw roundtrip scores. david often talks of one way scores (<= 20 is good), but + # the client reports scores as roundtrip and the server uses those values throughout + GOOD_SCORE = '.-40' + MODERATE_SCORE = '40-80' + POOR_SCORE = '80-120' + UNACCEPTABLE_SCORE = '120-.' + SCORED_SCORE = '.-.' # does not appear in menu choices + TEST_SCORE = '.-60' # does not appear in menu choices + ANY_SCORE = '' + M_SCORE_OPTS = [['Any', ANY_SCORE], ['Good', GOOD_SCORE], ['Moderate', MODERATE_SCORE], ['Poor', POOR_SCORE], ['Unacceptable', UNACCEPTABLE_SCORE]] + M_SCORE_DEFAULT = ANY_SCORE + F_SORT_RECENT = ['Most Recent', :date] F_SORT_OLDEST = ['Most Liked', :likes] F_SORT_LENGTH = ['Most Played', :plays] @@ -120,44 +132,137 @@ module JamRuby ordering.blank? ? keys[0] : keys.detect { |oo| oo.to_s == ordering } end - def self.musician_filter(params={}, current_user=nil) - rel = User.musicians + # produce a list of musicians (users where musician is true) + # params: + # instrument - instrument to search for or blank + # score_limit - a range specification for score, see M_SCORE_OPTS above. + # handled by relation_pagination: + # page - page number to fetch (origin 1) + # per_page - number of entries per page + # handled by order_param: + # orderby - what sort of search, also defines order (followed, plays, playing) + # previously handled by where_latlng: + # distance - defunct! + # city - defunct! + # remote_ip - defunct! + def self.musician_filter(params={}, user=nil, conn=nil) + # puts "================ params #{params.inspect}" + # puts "================ user #{user.inspect}" + # puts "================ conn #{conn.inspect}" + + rel = User.musicians_geocoded + rel = rel.select('users.*') + rel = rel.group('users.id') + unless (instrument = params[:instrument]).blank? - rel = rel.joins("RIGHT JOIN musicians_instruments AS minst ON minst.user_id = users.id") - .where(['minst.instrument_id = ? AND users.id IS NOT NULL', instrument]) + rel = rel.joins("inner JOIN musicians_instruments AS minst ON minst.user_id = users.id") + .where(['minst.instrument_id = ?', instrument]) end - rel = MaxMindGeo.where_latlng(rel, params, current_user) + # to find appropriate musicians we need to join users with scores to get to those with no scores or bad scores + # weeded out - sel_str = 'users.*' - case ordering = self.order_param(params) - when :plays # FIXME: double counting? - sel_str = "COUNT(records)+COUNT(sessions) AS play_count, #{sel_str}" - rel = rel.joins("LEFT JOIN music_sessions AS sessions ON sessions.user_id = users.id") - .joins("LEFT JOIN recordings AS records ON records.owner_id = users.id") - .group("users.id") - .order("play_count DESC, users.created_at DESC") - when :followed - sel_str = "COUNT(follows) AS search_follow_count, #{sel_str}" - rel = rel.joins("LEFT JOIN follows ON follows.followable_id = users.id") - .group("users.id") - .order("COUNT(follows) DESC, users.created_at DESC") - when :playing - rel = rel.joins("LEFT JOIN connections ON connections.user_id = users.id") - .where(['connections.music_session_id IS NOT NULL AND connections.aasm_state != ?', - 'expired']) - .order("users.created_at DESC") + # filter on scores using selections from params + # see M_SCORE_OPTS + score_limit = TEST_SCORE + l = params[:score_limit] + unless l.nil? + score_limit = l end - rel = rel.select(sel_str) + # puts "================ score_limit #{score_limit}" + + locidispid = ((conn and conn.client_type == 'client') ? conn.locidispid : ((user and user.musician) ? user.last_jam_locidispid : nil)) + + # puts "================ locidispid #{locidispid}" + + unless locidispid.nil? + # score_join of left allows for null scores, whereas score_join of inner requires a score however good or bad + # this is ANY_SCORE: + score_join = 'left' # or 'inner' + score_min = nil + score_max = nil + case score_limit + when GOOD_SCORE + score_join = 'inner' + score_min = nil + score_max = 40 + when MODERATE_SCORE + score_join = 'inner' + score_min = 40 + score_max = 80 + when POOR_SCORE + score_join = 'inner' + score_min = 80 + score_max = 120 + when UNACCEPTABLE_SCORE + score_join = 'inner' + score_min = 120 + score_max = nil + when SCORED_SCORE + score_join = 'inner' + score_min = nil + score_max = nil + when TEST_SCORE + score_join = 'inner' + score_min = nil + score_max = 60 + when ANY_SCORE + # the default of ANY setup above applies + else + # the default of ANY setup above applies + end + + rel = rel.joins("#{score_join} join scores on scores.alocidispid = users.last_jam_locidispid") + .where(['scores.blocidispid = ?', locidispid]) + + rel = rel.where(['scores.score > ?', score_min]) unless score_min.nil? + rel = rel.where(['scores.score <= ?', score_max]) unless score_max.nil? + + rel = rel.select('scores.score') + rel = rel.group('scores.score') + end + + ordering = self.order_param(params) + # puts "================ ordering #{ordering}" + case ordering + when :plays # FIXME: double counting? + # sel_str = "COUNT(records)+COUNT(sessions) AS play_count, #{sel_str}" + rel = rel.select('COUNT(records.id)+COUNT(sessions.id) AS search_play_count') + rel = rel.joins("LEFT JOIN music_sessions AS sessions ON sessions.user_id = users.id") + rel = rel.joins("LEFT JOIN recordings AS records ON records.owner_id = users.id") + rel = rel.order("search_play_count DESC") + when :followed + rel = rel.joins('left outer join follows on follows.followable_id = users.id') + rel = rel.select('count(follows.user_id) as search_follow_count') + rel = rel.order('search_follow_count DESC') + when :playing + rel = rel.joins("inner JOIN connections ON connections.user_id = users.id") + rel = rel.where(['connections.aasm_state != ?', 'expired']) + end + + unless locidispid.nil? + rel = rel.order('scores.score ASC NULLS LAST') + end + + rel = rel.order('users.created_at DESC') + + # rel = rel.select(sel_str) rel, page = self.relation_pagination(rel, params) rel = rel.includes([:instruments, :followings, :friends]) + # puts "======================== sql #{rel.to_sql}" objs = rel.all + # puts "======================== objs #{objs.inspect}" + # if objs.length > 0 + # puts "======================== attributes #{objs[0].attributes}" + # puts "======================== score #{objs[0].score}" + # end + srch = Search.new srch.search_type = :musicians_filter srch.page_num, srch.page_count = page, objs.total_pages - srch.musician_results_for_user(objs, current_user) + srch.musician_results_for_user(objs, user) end def self.relation_pagination(rel, params) @@ -273,13 +378,28 @@ module JamRuby false end - def self.new_musicians(usr, since_date=Time.now - 1.week, max_count=50, radius=M_MILES_DEFAULT) - rel = User.musicians + def self.new_musicians(usr, since_date) + # this attempts to find interesting musicians to tell another musician about where interesting + # is "has a good score and was created recently" + # we're sort of depending upon usr being a musicians_geocoded as well... + # this appears to only be called from EmailBatchNewMusician#deliver_batch_sets! which is + # an offline process and thus uses the last jam location as "home base" + + locidispid = usr.last_jam_locidispid + score_limit = 60 + limit = 50 + + rel = User.musicians_geocoded .where(['created_at >= ? AND users.id != ?', since_date, usr.id]) - .within(radius, :origin => [usr.lat, usr.lng]) - .order('created_at DESC') - .limit(max_count) + .joins('inner join scores on users.last_jam_locidispid = scores.alocidispid') + .where(['scores.blocidispid = ?', locidispid]) + .where(['scores.score <= ?', score_limit]) + .order('scores.score') # best scores first + .order('users.created_at DESC') # then most recent + .limit(limit) + objs = rel.all.to_a + if block_given? yield(objs) if 0 < objs.count else diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 17b9910f1..ff061e29b 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -12,13 +12,16 @@ module JamRuby include Geokit::ActsAsMappable::Glue unless defined?(acts_as_mappable) acts_as_mappable - after_save :check_lat_lng + # after_save :check_lat_lng - attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection, :lat, :lng + attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection # updating_password corresponds to a lost_password attr_accessor :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json + # used for temporary data store of latency between self and some other user + attr_accessor :latency_store + belongs_to :icecast_server_group, class_name: "JamRuby::IcecastServerGroup", inverse_of: :users, foreign_key: 'icecast_server_group_id' # authorizations (for facebook, etc -- omniauth) @@ -140,7 +143,7 @@ module JamRuby scope :musicians, where(:musician => true) scope :fans, where(:musician => false) - scope :geocoded_users, where(['lat IS NOT NULL AND lng IS NOT NULL']) + scope :geocoded_users, where(User.arel_table[:last_jam_locidispid].not_eq(nil)) scope :musicians_geocoded, musicians.geocoded_users scope :email_opt_in, where(:subscribe_email => true) @@ -221,11 +224,11 @@ module JamRuby end def location= location_hash - unless location_hash.blank? + unless location_hash.nil? self.city = location_hash[:city] self.state = location_hash[:state] self.country = location_hash[:country] - end if self.city.blank? + end end def musician? @@ -290,6 +293,11 @@ module JamRuby self.music_sessions.size end + def joined_score + nil unless has_attribute?(:score) + read_attribute(:score).to_i + end + # mods comes back as text; so give ourselves a parsed version def mods_json @mods_json ||= mods ? JSON.parse(mods, symbolize_names: true) : {} @@ -776,12 +784,20 @@ module JamRuby end user.admin = false - user.city = location[:city] - user.state = location[:state] - user.country = location[:country] + user.location = location + # user.city = location[:city] + # user.state = location[:state] + # user.country = location[:country] user.birth_date = birth_date - if user.musician # only update instruments if the user is a musician + if musician + user.last_jam_addr = location[:addr] + user.last_jam_locidispid = location[:locidispid] + user.last_jam_updated_reason = 'r' + user.last_jam_updated_at = Time.now + end + + if musician # only update instruments if the user is a musician unless instruments.nil? instruments.each do |musician_instrument_param| instrument = Instrument.find(musician_instrument_param[:instrument_id]) @@ -1095,55 +1111,64 @@ module JamRuby !self.city.blank? && (!self.state.blank? || !self.country.blank?) end - def check_lat_lng - if (city_changed? || state_changed? || country_changed?) && !lat_changed? && !lng_changed? - update_lat_lng - end - end + # def check_lat_lng + # if (city_changed? || state_changed? || country_changed?) && !lat_changed? && !lng_changed? + # update_lat_lng + # end + # end - def update_lat_lng(ip_addy=nil) - if provides_location? # ip_addy argument ignored in this case - return false unless ip_addy.nil? # do nothing if attempting to set latlng from an ip address - query = { :city => self.city } - query[:region] = self.state unless self.state.blank? - query[:country] = self.country unless self.country.blank? - if geo = MaxMindGeo.where(query).limit(1).first - geo.lat = nil if geo.lat = 0 - geo.lng = nil if geo.lng = 0 - if geo.lat && geo.lng && (self.lat != geo.lat || self.lng != geo.lng) - self.update_attributes({ :lat => geo.lat, :lng => geo.lng }) - return true - end - end - elsif ip_addy - if geo = MaxMindGeo.ip_lookup(ip_addy) - geo.lat = nil if geo.lat = 0 - geo.lng = nil if geo.lng = 0 - if self.lat != geo.lat || self.lng != geo.lng - self.update_attributes({ :lat => geo.lat, :lng => geo.lng }) - return true - end - end - else - if self.lat || self.lng - self.update_attributes({ :lat => nil, :lng => nil }) - return true - end - end - false - end + # def update_lat_lng(ip_addy=nil) + # if provides_location? # ip_addy argument ignored in this case + # return false unless ip_addy.nil? # do nothing if attempting to set latlng from an ip address + # query = { :city => self.city } + # query[:region] = self.state unless self.state.blank? + # query[:country] = self.country unless self.country.blank? + # if geo = MaxMindGeo.where(query).limit(1).first + # geo.lat = nil if geo.lat = 0 + # geo.lng = nil if geo.lng = 0 + # if geo.lat && geo.lng && (self.lat != geo.lat || self.lng != geo.lng) + # self.update_attributes({ :lat => geo.lat, :lng => geo.lng }) + # return true + # end + # end + # elsif ip_addy + # if geo = MaxMindGeo.ip_lookup(ip_addy) + # geo.lat = nil if geo.lat = 0 + # geo.lng = nil if geo.lng = 0 + # if self.lat != geo.lat || self.lng != geo.lng + # self.update_attributes({ :lat => geo.lat, :lng => geo.lng }) + # return true + # end + # end + # else + # if self.lat || self.lng + # self.update_attributes({ :lat => nil, :lng => nil }) + # return true + # end + # end + # false + # end def current_city(ip_addy=nil) - unless self.city - if self.lat && self.lng - # todo this is really dumb, you can't compare lat lng for equality - return MaxMindGeo.where(['lat = ? AND lng = ?',self.lat,self.lng]).limit(1).first.try(:city) - elsif ip_addy - return MaxMindGeo.ip_lookup(ip_addy).try(:city) - end - else - return self.city - end + # unless self.city + # if self.lat && self.lng + # # todo this is really dumb, you can't compare lat lng for equality + # return MaxMindGeo.where(['lat = ? AND lng = ?',self.lat,self.lng]).limit(1).first.try(:city) + # elsif ip_addy + # return MaxMindGeo.ip_lookup(ip_addy).try(:city) + # end + # else + # return self.city + # end + self.city + end + + def update_addr_loc(connection, reason) + self.last_jam_addr = connection.addr + self.last_jam_locidispid = connection.locidispid + self.last_jam_updated_reason = reason + self.last_jam_updated_at = Time.now + self.save end def top_followings @@ -1153,9 +1178,15 @@ module JamRuby .limit(3) end + def nearest_musicians + # FIXME: replace with Scotts scoring query + Search.new_musicians(self, Time.now - 1.week) + end + def self.deliver_new_musician_notifications(since_date=nil) since_date ||= Time.now-1.week - self.geocoded_users.find_each do |usr| + # return musicians with locidispid not null + self.musicians_geocoded.find_each do |usr| Search.new_musicians(usr, since_date) do |new_nearby| UserMailer.new_musicians(usr, new_nearby).deliver end diff --git a/ruby/lib/jam_ruby/resque/batch_email_job.rb b/ruby/lib/jam_ruby/resque/batch_email_job.rb new file mode 100644 index 000000000..6d82d420e --- /dev/null +++ b/ruby/lib/jam_ruby/resque/batch_email_job.rb @@ -0,0 +1,30 @@ +require 'resque' +require 'resque-lonely_job' + +module JamRuby + class BatchEmailJob + extend Resque::Plugins::LonelyJob + + @@log = Logging.logger[BatchEmailJob] + + @queue = :batch_emails + + def self.perform(batch_id) + if ebatch = EmailBatch.find_by_id(batch_id) + ebatch.deliver_batch + end + end + + def self.enqueue(batch_id) + begin + Resque.enqueue(self, batch_id) + true + rescue + # implies redis is down. but since there is no retry logic with this, we should at least log a warn in case we've configured something wrong + @@log.warn("unable to enqueue") + false + end + end + + end +end diff --git a/ruby/lib/jam_ruby/resque/scheduled/daily_session_emailer.rb b/ruby/lib/jam_ruby/resque/scheduled/daily_session_emailer.rb new file mode 100644 index 000000000..e5faa2d51 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/daily_session_emailer.rb @@ -0,0 +1,15 @@ +module JamRuby + class DailySessionEmailer + extend Resque::Plugins::LonelyJob + + @queue = :scheduled_daily_session_emailer + @@log = Logging.logger[DailySessionEmailer] + + def self.perform + @@log.debug("waking up") + EmailBatchScheduledSessions.send_daily_session_batch + @@log.debug("done") + end + + end +end diff --git a/ruby/lib/jam_ruby/resque/scheduled/email_error_collector.rb b/ruby/lib/jam_ruby/resque/scheduled/email_error_collector.rb deleted file mode 100644 index 0c6fc9397..000000000 --- a/ruby/lib/jam_ruby/resque/scheduled/email_error_collector.rb +++ /dev/null @@ -1,15 +0,0 @@ -module JamRuby - class EmailErrorCollector - extend Resque::Plugins::LonelyJob - - @queue = :email_error_collector - @@log = Logging.logger[EmailErrorCollector] - - def self.perform - @@log.debug("waking up") - EmailError.capture_errors - @@log.debug("done") - end - - end -end diff --git a/ruby/lib/jam_ruby/resque/scheduled/music_session_scheduler.rb b/ruby/lib/jam_ruby/resque/scheduled/music_session_scheduler.rb new file mode 100644 index 000000000..e69de29bb diff --git a/ruby/lib/jam_ruby/resque/scheduled/new_musician_emailer.rb b/ruby/lib/jam_ruby/resque/scheduled/new_musician_emailer.rb new file mode 100644 index 000000000..2a2c861a4 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/new_musician_emailer.rb @@ -0,0 +1,15 @@ +module JamRuby + class NewMusicianEmailer + extend Resque::Plugins::LonelyJob + + @queue = :scheduled_new_musician_emailer + @@log = Logging.logger[NewMusicianEmailer] + + def self.perform + @@log.debug("waking up") + EmailBatchNewMusician.send_new_musician_batch + @@log.debug("done") + end + + end +end diff --git a/ruby/lib/jam_ruby/resque/scheduled/user_progress_emailer.rb b/ruby/lib/jam_ruby/resque/scheduled/user_progress_emailer.rb new file mode 100644 index 000000000..99835146f --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/user_progress_emailer.rb @@ -0,0 +1,15 @@ +module JamRuby + class UserProgressEmailer + extend Resque::Plugins::LonelyJob + + @queue = :scheduled_user_progress_emailer + @@log = Logging.logger[UserProgressEmailer] + + def self.perform + @@log.debug("waking up") + EmailBatchProgression.send_progress_batch + @@log.debug("done") + end + + end +end diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index 3d11a6b54..8d3c6cb61 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -118,6 +118,7 @@ FactoryGirl.define do addr 0 locidispid 0 client_type 'client' + association :user, factory: :user end factory :invitation, :class => JamRuby::Invitation do @@ -451,6 +452,15 @@ FactoryGirl.define do test_emails 4.times.collect { Faker::Internet.safe_email }.join(',') end + factory :email_batch_new_musician, :class => JamRuby::EmailBatchNewMusician do + end + + factory :email_batch_progression, :class => JamRuby::EmailBatchProgression do + end + + factory :email_batch_scheduled_session, :class => JamRuby::EmailBatchScheduledSessions do + end + factory :notification, :class => JamRuby::Notification do factory :notification_text_message do @@ -468,7 +478,7 @@ FactoryGirl.define do factory :rsvp_slot, class: JamRuby::RsvpSlot do association :instrument, factory: :instrument association :music_session, factory: :music_session - association :rsvp_request_slot, factory: :rsvp_request_slot + # association :rsvp_request_slot, factory: :rsvp_request_slot proficiency_level 'beginner' end @@ -482,4 +492,19 @@ FactoryGirl.define do factory :rsvp_request_slot, class: JamRuby::RsvpRequestRsvpSlot do chosen false end + + factory :latency_tester, :class => JamRuby::LatencyTester do + ignore do + connection nil + make_connection true + end + + sequence(:client_id) { |n| "LatencyTesterClientId-#{n}" } + + after(:create) do |latency_tester, evaluator| + latency_tester.connection = evaluator.connection if evaluator.connection + latency_tester.connection = FactoryGirl.create(:connection, client_type: Connection::TYPE_LATENCY_TESTER, client_id: latency_tester.client_id) if evaluator.make_connection + latency_tester.save + end + end end diff --git a/ruby/spec/jam_ruby/models/active_music_session_spec.rb b/ruby/spec/jam_ruby/models/active_music_session_spec.rb index 74be88bf1..44b14fe27 100644 --- a/ruby/spec/jam_ruby/models/active_music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/active_music_session_spec.rb @@ -361,7 +361,7 @@ describe ActiveMusicSession do @music_session = FactoryGirl.create(:active_music_session, :creator => @user1, :musician_access => true) # @music_session.connections << @connection @music_session.save! - @connection.join_the_session(@music_session, true, nil) + @connection.join_the_session(@music_session, true, nil, @user1) end describe "not recording" do diff --git a/ruby/spec/jam_ruby/models/claimed_recording_spec.rb b/ruby/spec/jam_ruby/models/claimed_recording_spec.rb index 171749999..4bbbeea17 100644 --- a/ruby/spec/jam_ruby/models/claimed_recording_spec.rb +++ b/ruby/spec/jam_ruby/models/claimed_recording_spec.rb @@ -21,7 +21,7 @@ describe ClaimedRecording do @music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true) # @music_session.connections << @connection @music_session.save - @connection.join_the_session(@music_session, true, nil) + @connection.join_the_session(@music_session, true, nil, @user) @recording = Recording.start(@music_session, @user) @recording.stop @recording.reload diff --git a/ruby/spec/jam_ruby/models/email_batch_spec.rb b/ruby/spec/jam_ruby/models/email_batch_spec.rb index e3b14c32d..8cac2b44c 100644 --- a/ruby/spec/jam_ruby/models/email_batch_spec.rb +++ b/ruby/spec/jam_ruby/models/email_batch_spec.rb @@ -1,18 +1,464 @@ require 'spec_helper' describe EmailBatch do - let (:email_batch) { FactoryGirl.create(:email_batch) } - before(:each) do - BatchMailer.deliveries.clear + after(:each) do + Timecop.return end - it 'has test emails setup' do - expect(email_batch.test_emails.present?).to be true - expect(email_batch.pending?).to be true + describe 'all users' do + + # before { pending } - users = email_batch.test_users - expect(email_batch.test_count).to eq(users.count) + let (:email_batch) { FactoryGirl.create(:email_batch) } + + before(:each) do + BatchMailer.deliveries.clear + end + + it 'has test emails setup' do + + expect(email_batch.test_emails.present?).to be true + expect(email_batch.pending?).to be true + + users = email_batch.test_users + expect(email_batch.test_count).to eq(users.count) + end end + describe 'new musician' do + before { pending } + + let (:new_musician_batch) { FactoryGirl.create(:email_batch_new_musician) } + + before(:each) do + @u1 = FactoryGirl.create(:user, :lat => 37.791649, :lng => -122.394395, :email => 'jonathan@jamkazam.com', :subscribe_email => true, :created_at => Time.now - 3.weeks) + @u2 = FactoryGirl.create(:user, :lat => 37.791649, :lng => -122.394395, :subscribe_email => true) + @u3 = FactoryGirl.create(:user, :lat => 37.791649, :lng => -122.394395, :subscribe_email => false, :created_at => Time.now - 3.weeks) + @u4 = FactoryGirl.create(:user, :lat => 37.791649, :lng => -122.394395, :subscribe_email => true, :created_at => Time.now - 3.weeks) + end + + it 'find new musicians with good score' do + new_musician_batch.fetch_recipients do |new_musicians| + expect(new_musicians.count).to eq(2) + num = (new_musicians.keys.map(&:id) - [@u1.id, @u4.id]).count + expect(num).to eq(0) + end + end + + it 'cycles through states properly' do + new_musician_batch.deliver_batch + expect(UserMailer.deliveries.length).to eq(2) + new_musician_batch.reload + expect(new_musician_batch.delivered?).to eq(true) + expect(new_musician_batch.sent_count).to eq(2) + end + + end + + context 'user progress' do + + # before { pending } + + def handles_new_users(ebatch, user) + 3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(0) } + + dd = user.created_at + ebatch.days_past_for_trigger_index(0).days + Timecop.travel(dd) + vals = [1,0,0] + 3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(vals[nn]) } + ebatch.make_set(user, 0) + 3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(0) } + + dd = dd + ebatch.days_past_for_trigger_index(1).days + Timecop.travel(dd) + vals = [0,1,0] + 3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(vals[nn]) } + ebatch.make_set(user, 1) + 3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(0) } + + dd = dd + ebatch.days_past_for_trigger_index(2).days + Timecop.travel(dd) + vals = [0,0,1] + 3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(vals[nn]) } + ebatch.make_set(user, 2) + 3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(0) } + end + + def handles_existing_users(ebatch, user) + vals = [1,0,0] + 3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(vals[nn]) } + + dd = user.created_at + ebatch.days_past_for_trigger_index(0).days + Timecop.travel(dd) + ebatch.make_set(user, 0) + 3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(0) } + + dd = dd + ebatch.days_past_for_trigger_index(1).days + Timecop.travel(dd) + vals = [0,1,0] + 3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(vals[nn]) } + ebatch.make_set(user, 1) + + dd = dd + ebatch.days_past_for_trigger_index(2).days + Timecop.travel(dd) + expect(ebatch.fetch_recipients(2).count).to eq(1) + ebatch.make_set(user, 2) + expect(ebatch.fetch_recipients(2).count).to eq(0) + + dd = dd + 1 + Timecop.travel(dd) + expect(ebatch.fetch_recipients(2).count).to eq(0) + end + + def skips_some_days(ebatch, user) + dd = user.created_at + ebatch.days_past_for_trigger_index(1).days + Timecop.travel(dd) + vals = [1,0,0] + 3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(vals[nn]) } + ebatch.make_set(user, 0) + 2.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(0) } + + dd = dd + ebatch.days_past_for_trigger_index(1).days + Timecop.travel(dd) + vals = [0,1,0] + 3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(vals[nn]) } + ebatch.make_set(user, 1) + expect(ebatch.fetch_recipients(2).count).to eq(0) + + dd = dd + ebatch.days_past_for_trigger_index(2).days + Timecop.travel(dd) + vals = [0,0,1] + 3.times { |nn| expect(ebatch.fetch_recipients(nn).count).to eq(vals[nn]) } + ebatch.make_set(user, 2) + expect(ebatch.fetch_recipients(2).count).to eq(0) + end + + def loops_bunch_of_users(ebatch, users) + expect(ebatch.fetch_recipients(0,5).count).to eq(0) + dd = users[0].created_at + ebatch.days_past_for_trigger_index(0).days + Timecop.travel(dd) + expect(ebatch.fetch_recipients(0,5).count).to eq(20) + users.each { |uu| ebatch.make_set(uu, 0) } + expect(ebatch.fetch_recipients(0,5).count).to eq(0) + users.map &:destroy + end + + def sends_one_email(existing_user, ebatch) + ProgressMailer.deliveries.clear + ebatch.deliver_batch + expect(ProgressMailer.deliveries.length).to eq(1) + end + + describe 'client_notdl' do + # before { pending } + let(:batchp) { + FactoryGirl.create(:email_batch_progression, :sub_type => :client_notdl) + } + let(:user_) { FactoryGirl.create(:user) } + let(:user_existing) { + FactoryGirl.create(:user, + :created_at => Time.now - (2 * batchp.days_past_for_trigger_index(2)).days) + } + after(:each) do + batchp.clear_batch_sets! + Timecop.return + end + it 'sends one email' do + sends_one_email(user_existing, batchp) + end + it 'handles new users' do + handles_new_users(batchp, user_) + end + it 'handles existing users' do + handles_existing_users(batchp, user_existing) + end + it 'skips some days' do + skips_some_days(batchp, user_) + end + it 'loops bunch of users' do + users = [] + 20.times { |nn| users << FactoryGirl.create(:user) } + loops_bunch_of_users(batchp, users) + end + end + + describe 'client_dl_notrun' do + # before { pending } + let(:batchp) { + FactoryGirl.create(:email_batch_progression, :sub_type => :client_dl_notrun) + } + let(:user_) { FactoryGirl.create(:user, :first_downloaded_client_at => Time.now) } + let(:date_in_past) { Time.now - (2 * batchp.days_past_for_trigger_index(2)).days } + let(:user_existing) { + FactoryGirl.create(:user, + :created_at => date_in_past, + :first_downloaded_client_at => date_in_past) + } + after(:each) do + batchp.clear_batch_sets! + Timecop.return + end + it 'sends one email' do + sends_one_email(user_existing, batchp) + end + it 'handles new users' do + handles_new_users(batchp, user_) + end + it 'handles existing users' do + handles_existing_users(batchp, user_existing) + end + it 'skips some days' do + skips_some_days(batchp, user_) + end + it 'loops bunch of users' do + users = [] + 20.times { |nn| users << FactoryGirl.create(:user, :first_downloaded_client_at => Time.now) } + loops_bunch_of_users(batchp, users) + end + end + + describe 'client_run_notgear' do + # before { pending } + let(:batchp) { + FactoryGirl.create(:email_batch_progression, :sub_type => :client_run_notgear) + } + let(:user_) { FactoryGirl.create(:user, :first_ran_client_at => Time.now) } + let(:date_in_past) { Time.now - (2 * batchp.days_past_for_trigger_index(2)).days } + let(:user_existing) { + FactoryGirl.create(:user, + :created_at => date_in_past, + :first_ran_client_at => date_in_past) + } + after(:each) do + batchp.clear_batch_sets! + Timecop.return + end + it 'sends one email' do + sends_one_email(user_existing, batchp) + end + it 'handles new users' do + handles_new_users(batchp, user_) + end + it 'handles existing users' do + handles_existing_users(batchp, user_existing) + end + it 'skips some days' do + skips_some_days(batchp, user_) + end + it 'loops bunch of users' do + users = [] + 20.times { |nn| users << FactoryGirl.create(:user, :first_ran_client_at => Time.now) } + loops_bunch_of_users(batchp, users) + end + end + + describe 'gear_notsess' do + # before { pending } + let(:batchp) { + FactoryGirl.create(:email_batch_progression, :sub_type => :gear_notsess) + } + let(:user_) { FactoryGirl.create(:user, :first_certified_gear_at => Time.now) } + let(:date_in_past) { Time.now - (2 * batchp.days_past_for_trigger_index(2)).days } + let(:user_existing) { + FactoryGirl.create(:user, + :created_at => date_in_past, + :first_certified_gear_at => date_in_past) + } + after(:each) do + batchp.clear_batch_sets! + Timecop.return + end + it 'sends one email' do + sends_one_email(user_existing, batchp) + end + it 'handles new users' do + handles_new_users(batchp, user_) + end + it 'handles existing users' do + handles_existing_users(batchp, user_existing) + end + it 'skips some days' do + skips_some_days(batchp, user_) + end + it 'loops bunch of users' do + users = [] + 20.times { |nn| users << FactoryGirl.create(:user, :first_certified_gear_at => Time.now) } + loops_bunch_of_users(batchp, users) + end + end + + describe 'sess_notgood' do + # before { pending } + let(:batchp) { + FactoryGirl.create(:email_batch_progression, :sub_type => :sess_notgood) + } + let(:user_) { FactoryGirl.create(:user, :first_real_music_session_at => Time.now) } + let(:date_in_past) { Time.now - (2 * batchp.days_past_for_trigger_index(2)).days } + let(:user_existing) { + FactoryGirl.create(:user, + :created_at => date_in_past, + :first_real_music_session_at => date_in_past) + } + after(:each) do + batchp.clear_batch_sets! + Timecop.return + end + it 'sends one email' do + sends_one_email(user_existing, batchp) + end + it 'handles new users' do + handles_new_users(batchp, user_) + end + it 'handles existing users' do + handles_existing_users(batchp, user_existing) + end + it 'skips some days' do + skips_some_days(batchp, user_) + end + it 'loops bunch of users' do + users = [] + 20.times { |nn| users << FactoryGirl.create(:user, :first_real_music_session_at => Time.now) } + loops_bunch_of_users(batchp, users) + end + end + + describe 'sess_notrecord' do + # before { pending } + let(:batchp) { + FactoryGirl.create(:email_batch_progression, :sub_type => :sess_notrecord) + } + let(:user_) { FactoryGirl.create(:user, :first_real_music_session_at => Time.now) } + let(:date_in_past) { Time.now - (2 * batchp.days_past_for_trigger_index(2)).days } + let(:user_existing) { + FactoryGirl.create(:user, + :created_at => date_in_past, + :first_real_music_session_at => date_in_past) + } + after(:each) do + batchp.clear_batch_sets! + Timecop.return + end + it 'sends one email' do + sends_one_email(user_existing, batchp) + end + it 'handles new users' do + handles_new_users(batchp, user_) + end + it 'handles existing users' do + handles_existing_users(batchp, user_existing) + end + it 'skips some days' do + skips_some_days(batchp, user_) + end + it 'loops bunch of users' do + users = [] + 20.times { |nn| users << FactoryGirl.create(:user, :first_real_music_session_at => Time.now) } + loops_bunch_of_users(batchp, users) + end + end + + describe 'reg_notinvite' do + # before { pending } + let(:batchp) { + FactoryGirl.create(:email_batch_progression, :sub_type => :reg_notinvite) + } + let(:user_) { FactoryGirl.create(:user) } + let(:date_in_past) { Time.now - (2 * batchp.days_past_for_trigger_index(2)).days } + let(:user_existing) { + FactoryGirl.create(:user, + :created_at => date_in_past) + } + after(:each) do + batchp.clear_batch_sets! + Timecop.return + end + it 'sends one email' do + sends_one_email(user_existing, batchp) + end + it 'handles new users' do + handles_new_users(batchp, user_) + end + it 'handles existing users' do + handles_existing_users(batchp, user_existing) + end + it 'skips some days' do + skips_some_days(batchp, user_) + end + it 'loops bunch of users' do + users = [] + 20.times { |nn| users << FactoryGirl.create(:user) } + loops_bunch_of_users(batchp, users) + end + end + + describe 'reg_notconnect' do + # before { pending } + let(:batchp) { + FactoryGirl.create(:email_batch_progression, :sub_type => :reg_notconnect) + } + let(:user_) { FactoryGirl.create(:user) } + let(:date_in_past) { Time.now - (2 * batchp.days_past_for_trigger_index(2)).days } + let(:user_existing) { + FactoryGirl.create(:user, + :created_at => date_in_past) + } + after(:each) do + batchp.clear_batch_sets! + Timecop.return + end + it 'sends one email' do + sends_one_email(user_existing, batchp) + end + it 'handles new users' do + handles_new_users(batchp, user_) + end + it 'handles existing users' do + handles_existing_users(batchp, user_existing) + end + it 'skips some days' do + skips_some_days(batchp, user_) + end + it 'loops bunch of users' do + users = [] + 20.times { |nn| users << FactoryGirl.create(:user) } + loops_bunch_of_users(batchp, users) + end + end + + describe 'reg_notlike' do + # before { pending } + let(:batchp) { + FactoryGirl.create(:email_batch_progression, :sub_type => :reg_notlike) + } + let(:user_) { FactoryGirl.create(:user) } + let(:date_in_past) { Time.now - (2 * batchp.days_past_for_trigger_index(2)).days } + let(:user_existing) { + FactoryGirl.create(:user, + :created_at => date_in_past) + } + after(:each) do + batchp.clear_batch_sets! + Timecop.return + end + it 'sends one email' do + sends_one_email(user_existing, batchp) + end + it 'handles new users' do + handles_new_users(batchp, user_) + end + it 'handles existing users' do + handles_existing_users(batchp, user_existing) + end + it 'skips some days' do + skips_some_days(batchp, user_) + end + it 'loops bunch of users' do + users = [] + 20.times { |nn| users << FactoryGirl.create(:user) } + loops_bunch_of_users(batchp, users) + end + end + + end end diff --git a/ruby/spec/jam_ruby/models/email_batch_spec_new_musicians.rb b/ruby/spec/jam_ruby/models/email_batch_spec_new_musicians.rb new file mode 100644 index 000000000..e259ed865 --- /dev/null +++ b/ruby/spec/jam_ruby/models/email_batch_spec_new_musicians.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe EmailBatchNewMusician do + + after(:each) do + Timecop.return + end + + before(:each) do + UserMailer.deliveries.clear + end + + describe 'daily scheduled' do + # before { pending } + + let (:new_musician_batch) { FactoryGirl.create(:email_batch_new_musician) } + + let (:receiver) { + FactoryGirl.create(:user, + :created_at => Time.now - (EmailBatchNewMusician::SINCE_DAYS.days + 1.day), + :last_jam_locidispid => 1, + :last_jam_addr => 1 + ) + } + + let (:drummer) { FactoryGirl.create(:user, + :last_jam_locidispid => 1, + :last_jam_addr => 1) } + let (:guitarist) { FactoryGirl.create(:user, + :last_jam_locidispid => 1, + :last_jam_addr => 1) } + let (:bassist) { FactoryGirl.create(:user, + :last_jam_locidispid => 1, + :last_jam_addr => 1) } + let (:vocalist) { FactoryGirl.create(:user, + :last_jam_locidispid => 1, + :last_jam_addr => 1) } + let (:loser) { FactoryGirl.create(:user, + :last_jam_locidispid => 2, + :last_jam_addr => 2) } + before(:each) do + + JamRuby::Score.createx(1, 'a', 1, 1, 'a', 1, 10) + JamRuby::Score.createx(1, 'a', 1, 2, 'a', 2, Score::MAX_YELLOW_LATENCY + 1) + end + + it 'sets up data properly' do + receiver; drummer; loser; vocalist + results = new_musician_batch.fetch_recipients + expect(results.count).to eq(1) + user, new_musicians = results[0] + expect(user.id).to eq(receiver.id) + expect(new_musicians.count).to eq(2) + end + + it 'sends email' do + pending + ebatch = new_musician_batch + ebatch.deliver_batch + expect(UserMailer.deliveries.length).to eq(3) + end + + end +end diff --git a/ruby/spec/jam_ruby/models/email_batch_spec_scheduled_session.rb b/ruby/spec/jam_ruby/models/email_batch_spec_scheduled_session.rb new file mode 100644 index 000000000..316c65808 --- /dev/null +++ b/ruby/spec/jam_ruby/models/email_batch_spec_scheduled_session.rb @@ -0,0 +1,106 @@ +require 'spec_helper' + +describe EmailBatchScheduledSessions do + + after(:each) do + Timecop.return + end + + before(:each) do + UserMailer.deliveries.clear + end + + describe 'daily scheduled' do + # before { pending } + + let (:scheduled_batch) { FactoryGirl.create(:email_batch_scheduled_session) } + + let (:drums) { FactoryGirl.create(:instrument, :description => 'drums') } + let (:guitar) { FactoryGirl.create(:instrument, :description => 'guitar') } + let (:bass) { FactoryGirl.create(:instrument, :description => 'bass') } + let (:vocals) { FactoryGirl.create(:instrument, :description => 'vocal') } + + let (:drummer) { FactoryGirl.create(:user, + :last_jam_locidispid => 1, + :last_jam_addr => 1) } + let (:guitarist) { FactoryGirl.create(:user, + :last_jam_locidispid => 1, + :last_jam_addr => 1) } + let (:bassist) { FactoryGirl.create(:user, + :last_jam_locidispid => 1, + :last_jam_addr => 1) } + let (:vocalist) { FactoryGirl.create(:user, + :last_jam_locidispid => 1, + :last_jam_addr => 1) } + let (:loser) { FactoryGirl.create(:user, + :last_jam_locidispid => 2, + :last_jam_addr => 2) } + + let (:session1) do + FactoryGirl.create(:music_session, + :creator => drummer, + :scheduled_start => Time.now() + 2.days, + :musician_access => true, + :approval_required => false) + end + let (:session2) do + FactoryGirl.create(:music_session, + :creator => drummer, + :scheduled_start => Time.now() + 2.days, + :musician_access => true, + :approval_required => false) + end + + before(:each) do + MusicianInstrument.delete_all + RsvpSlot.delete_all + JamRuby::Score.delete_all + + drummer.musician_instruments << FactoryGirl.build(:musician_instrument, user: drummer, instrument: drums, proficiency_level: 2) + drummer.musician_instruments << FactoryGirl.build(:musician_instrument, user: drummer, instrument: guitar, proficiency_level: 2) + + guitarist.musician_instruments << FactoryGirl.build(:musician_instrument, user: guitarist, instrument: guitar, proficiency_level: 2) + guitarist.musician_instruments << FactoryGirl.build(:musician_instrument, user: guitarist, instrument: bass, proficiency_level: 2) + + bassist.musician_instruments << FactoryGirl.build(:musician_instrument, user: bassist, instrument: bass, proficiency_level: 2) + bassist.musician_instruments << FactoryGirl.build(:musician_instrument, user: bassist, instrument: guitar, proficiency_level: 2) + + vocalist.musician_instruments << FactoryGirl.build(:musician_instrument, user: vocalist, instrument: vocals, proficiency_level: 2) + + loser.musician_instruments << FactoryGirl.build(:musician_instrument, user: loser, instrument: vocals, proficiency_level: 2) + loser.musician_instruments << FactoryGirl.build(:musician_instrument, user: loser, instrument: drums, proficiency_level: 2) + + FactoryGirl.create(:rsvp_slot, :instrument => drums, :music_session => session1) + FactoryGirl.create(:rsvp_slot, :instrument => guitar, :music_session => session1) + FactoryGirl.create(:rsvp_slot, :instrument => bass, :music_session => session1) + + FactoryGirl.create(:rsvp_slot, :instrument => drums, :music_session => session2) + FactoryGirl.create(:rsvp_slot, :instrument => guitar, :music_session => session2) + FactoryGirl.create(:rsvp_slot, :instrument => bass, :music_session => session2) + # oo = FactoryGirl.create(:rsvp_slot, :instrument => vocals, :music_session => session1) + # oo.rsvp_request_slot.update_attributes(chosen: true) + # oo = FactoryGirl.create(:rsvp_request, :user => vocalist, :rsvp_slot => oo) + # oo.rsvp_request_slot.update_attributes(chosen: true) + + JamRuby::Score.createx(1, 'a', 1, 1, 'a', 1, 10) + JamRuby::Score.createx(1, 'a', 1, 2, 'a', 2, Score::MAX_YELLOW_LATENCY + 1) + end + + before(:each) do + end + + it 'sets up data properly' do + expect(drummer.instruments.include?(drums)).to eq(true) + expect(drummer.instruments.include?(guitar)).to eq(true) + obj = scheduled_batch.fetch_recipients + expect(obj.count).to eq(3) + end + + it 'sends email' do + ebatch = scheduled_batch + ebatch.deliver_batch + expect(UserMailer.deliveries.length).to eq(3) + end + + end +end diff --git a/ruby/spec/jam_ruby/models/get_work_spec.rb b/ruby/spec/jam_ruby/models/get_work_spec.rb index 9f8349b33..4e12d2ab0 100644 --- a/ruby/spec/jam_ruby/models/get_work_spec.rb +++ b/ruby/spec/jam_ruby/models/get_work_spec.rb @@ -7,13 +7,13 @@ describe GetWork do end it "get_work_1" do - x = GetWork.get_work(1) + x = GetWork.get_work(1, 0) #puts x.inspect x.should be_nil end it "get_work_list_1" do - x = GetWork.get_work_list(1) + x = GetWork.get_work_list(1, 0) #puts x.inspect x.should eql([]) end diff --git a/ruby/spec/jam_ruby/models/latency_tester_spec.rb b/ruby/spec/jam_ruby/models/latency_tester_spec.rb new file mode 100644 index 000000000..e0d3071d2 --- /dev/null +++ b/ruby/spec/jam_ruby/models/latency_tester_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe LatencyTester do + + let(:params) {{client_id: 'abc', ip_address: '10.1.1.1', connection_stale_time:40, connection_expire_time:60} } + + it "success" do + latency_tester = FactoryGirl.create(:latency_tester) + latency_tester.connection.should_not be_nil + latency_tester.connection.latency_tester.should_not be_nil + + end + + describe "connect" do + it "no existing latency tester" do + latency_tester = LatencyTester.connect(params) + latency_tester.errors.any?.should be_false + latency_tester.connection.ip_address.should == params[:ip_address] + latency_tester.connection.stale_time.should == params[:connection_stale_time] + latency_tester.connection.expire_time.should == params[:connection_expire_time] + end + + it "existing latency tester, no connection" do + latency_tester = FactoryGirl.create(:latency_tester, client_id: params[:client_id], make_connection: false) + latency_tester.connection.should be_nil + + latency_tester.client_id = params[:client_id] + latency_tester.save! + + found = LatencyTester.connect(params) + found.should == latency_tester + found.connection.should_not be_nil + found.connection.aasm_state.should == Connection::CONNECT_STATE.to_s + found.connection.client_id.should == latency_tester.client_id + end + + it "existing latency tester, existing connection" do + latency_tester = FactoryGirl.create(:latency_tester) + + latency_tester.connection.aasm_state = Connection::STALE_STATE.to_s + latency_tester.save! + set_updated_at(latency_tester.connection, 1.days.ago) + + params[:client_id] = latency_tester.connection.client_id + + found = LatencyTester.connect(params) + found.should == latency_tester + found.connection.should == latency_tester.connection + # state should have refreshed from stale to connected + found.connection.aasm_state.should == Connection::CONNECT_STATE.to_s + # updated_at needs to be poked on connection to keep stale non-stale + (found.connection.updated_at - latency_tester.connection.updated_at).to_i.should == 60 * 60 * 24 # 1 day + end + end +end diff --git a/ruby/spec/jam_ruby/models/mix_spec.rb b/ruby/spec/jam_ruby/models/mix_spec.rb index eaa65bd63..904c036a4 100755 --- a/ruby/spec/jam_ruby/models/mix_spec.rb +++ b/ruby/spec/jam_ruby/models/mix_spec.rb @@ -10,7 +10,7 @@ describe Mix do @music_session = FactoryGirl.create(:active_music_session, :creator => @user, :musician_access => true) # @music_session.connections << @connection @music_session.save - @connection.join_the_session(@music_session, true, nil) + @connection.join_the_session(@music_session, true, nil, @user) @recording = Recording.start(@music_session, @user) @recording.stop @recording.claim(@user, "name", "description", Genre.first, true) diff --git a/ruby/spec/jam_ruby/models/music_session_spec.rb b/ruby/spec/jam_ruby/models/music_session_spec.rb index a52688b26..152e957de 100644 --- a/ruby/spec/jam_ruby/models/music_session_spec.rb +++ b/ruby/spec/jam_ruby/models/music_session_spec.rb @@ -46,7 +46,6 @@ describe MusicSession do music_session.valid?.should be_false end end - end diff --git a/ruby/spec/jam_ruby/models/musician_search_spec.rb b/ruby/spec/jam_ruby/models/musician_search_spec.rb index e957b9c46..1b60aae49 100644 --- a/ruby/spec/jam_ruby/models/musician_search_spec.rb +++ b/ruby/spec/jam_ruby/models/musician_search_spec.rb @@ -3,32 +3,88 @@ require 'spec_helper' describe 'Musician search' do before(:each) do - @geocode1 = FactoryGirl.create(:geocoder) - @geocode2 = FactoryGirl.create(:geocoder) - @users = [] - @users << @user1 = FactoryGirl.create(:user) - @users << @user2 = FactoryGirl.create(:user) - @users << @user3 = FactoryGirl.create(:user) - @users << @user4 = FactoryGirl.create(:user) + # @geocode1 = FactoryGirl.create(:geocoder) + # @geocode2 = FactoryGirl.create(:geocoder) + 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 + + @geomusicians = [] + @geomusicians << @user1 + @geomusicians << @user2 + @geomusicians << @user3 + @geomusicians << @user4 + @geomusicians << @user5 + + Score.delete_all + 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 - context 'default filter settings' do + context 'default filter settings' do it "finds all musicians" do - # expects all the users - num = User.musicians.count - results = Search.musician_filter({ :per_page => num }) - expect(results.results.count).to eq(num) - expect(results.search_type).to be(:musicians_filter) + # expects all the musicians (geocoded) + results = Search.musician_filter + results.search_type.should == :musicians_filter + results.results.count.should == @geomusicians.length + results.results.should eq @geomusicians.reverse end - it "finds musicians with proper ordering" do - # the ordering should be create_at since no followers exist - expect(Follow.count).to eq(0) - results = Search.musician_filter({ :per_page => User.musicians.count }) - results.results.each_with_index do |uu, idx| - expect(uu.id).to eq(@users.reverse[idx].id) - end + it "finds all musicians page 1" do + # expects all the musicians + results = Search.musician_filter({page: 1}) + results.search_type.should == :musicians_filter + results.results.count.should == @geomusicians.length + results.results.should eq @geomusicians.reverse + end + + it "finds all musicians page 2" do + # expects no musicians (all fit on page 1) + results = Search.musician_filter({page: 2}) + results.search_type.should == :musicians_filter + results.results.count.should == 0 + end + + it "finds all musicians page 1 per_page 3" do + # expects three of the musicians + results = Search.musician_filter({per_page: 3}) + results.search_type.should == :musicians_filter + results.results.count.should == 3 + results.results.should eq @geomusicians.reverse.slice(0, 3) + end + + it "finds all musicians page 2 per_page 3" do + # expects two of the musicians + results = Search.musician_filter({page: 2, per_page: 3}) + results.search_type.should == :musicians_filter + results.results.count.should == 2 + results.results.should eq @geomusicians.reverse.slice(3, 3) + end + + it "finds all musicians page 3 per_page 3" do + # expects two of the musicians + results = Search.musician_filter({page: 3, per_page: 3}) + results.search_type.should == :musicians_filter + results.results.count.should == 0 end it "sorts musicians by followers" do @@ -90,7 +146,7 @@ describe 'Musician search' do f3.save # @user2.followers.concat([@user3, @user4, @user2]) - results = Search.musician_filter({ :per_page => @users.size }, @user3) + results = Search.musician_filter({ :per_page => @musicians.size }, @user3) expect(results.results[0].id).to eq(@user2.id) # check the follower count for given entry @@ -109,13 +165,13 @@ describe 'Musician search' do end def make_recording(usr) - connection = FactoryGirl.create(:connection, :user => usr) + connection = FactoryGirl.create(:connection, :user => usr, locidispid: usr.last_jam_locidispid) instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') track = FactoryGirl.create(:track, :connection => connection, :instrument => instrument) music_session = FactoryGirl.create(:active_music_session, :creator => usr, :musician_access => true) # music_session.connections << connection - music_session.save - connection.join_the_session(music_session, true, nil) + # music_session.save + connection.join_the_session(music_session, true, nil, usr) recording = Recording.start(music_session, usr) recording.stop recording.reload @@ -126,11 +182,11 @@ describe 'Musician search' do end def make_session(usr) - connection = FactoryGirl.create(:connection, :user => usr) + connection = FactoryGirl.create(:connection, :user => usr, locidispid: usr.last_jam_locidispid) music_session = FactoryGirl.create(:active_music_session, :creator => usr, :musician_access => true) # music_session.connections << connection - music_session.save - connection.join_the_session(music_session, true, nil) + # music_session.save + connection.join_the_session(music_session, true, nil, usr) end context 'musician stat counters' do @@ -154,8 +210,8 @@ describe 'Musician search' do # @user4.followers.concat([@user4]) # @user3.followers.concat([@user4]) # @user2.followers.concat([@user4]) - expect(@user4.top_followings.count).to be 3 - expect(@user4.top_followings.map(&:id)).to match_array((@users - [@user1]).map(&:id)) + expect(@user4.top_followings.count).to eq 3 + expect(@user4.top_followings.map(&:id)).to match_array((@musicians - [@user1, @user5, @user6]).map(&:id)) end it "friends stat shows friend count" do @@ -172,6 +228,8 @@ describe 'Musician search' do end it "recording stat shows recording count" do + Recording.delete_all + recording = make_recording(@user1) expect(recording.users.length).to be 1 expect(recording.users.first).to eq(@user1) @@ -182,6 +240,7 @@ describe 'Musician search' do expect(@user1.recordings.detect { |rr| rr == recording }).to_not be_nil results = Search.musician_filter({},@user1) + # puts "====================== results #{results.inspect}" uu = results.results.detect { |mm| mm.id == @user1.id } expect(uu).to_not be_nil @@ -194,31 +253,39 @@ describe 'Musician search' do context 'musician sorting' do it "by plays" do + Recording.delete_all + make_recording(@user1) # order results by num recordings results = Search.musician_filter({ :orderby => 'plays' }, @user2) + # puts "========= results #{results.inspect}" + expect(results.results.length).to eq(2) expect(results.results[0].id).to eq(@user1.id) + expect(results.results[1].id).to eq(@user3.id) # add more data and make sure order still correct - make_recording(@user2); make_recording(@user2) + make_recording(@user3) + make_recording(@user3) results = Search.musician_filter({ :orderby => 'plays' }, @user2) - expect(results.results[0].id).to eq(@user2.id) + expect(results.results.length).to eq(2) + expect(results.results[0].id).to eq(@user3.id) + expect(results.results[1].id).to eq(@user1.id) end it "by now playing" do # should get 1 result with 1 active session - make_session(@user3) + make_session(@user1) results = Search.musician_filter({ :orderby => 'playing' }, @user2) expect(results.results.count).to be 1 - expect(results.results.first.id).to eq(@user3.id) + expect(results.results.first.id).to eq(@user1.id) # should get 2 results with 2 active sessions # sort order should be created_at DESC - make_session(@user4) + make_session(@user3) results = Search.musician_filter({ :orderby => 'playing' }, @user2) expect(results.results.count).to be 2 - expect(results.results[0].id).to eq(@user4.id) - expect(results.results[1].id).to eq(@user3.id) + expect(results.results[0].id).to eq(@user3.id) + expect(results.results[1].id).to eq(@user1.id) end end @@ -275,28 +342,43 @@ describe 'Musician search' do context 'new users' do - it "find nearby" do - # create new user outside 500 from Apex to ensure its excluded from results - FactoryGirl.create(:user, {city: "Austin", state: "TX", country: "US"}) - User.geocoded_users.find_each do |usr| - Search.new_musicians(usr) do |new_usrs| - # the newly created user is not nearby the existing users (which are in Apex, NC) - # and that user is not included in query - expect(new_usrs.count).to eq(User.musicians.count - 1) - end - end + it "find three for user1" do + # user2..4 are scored against user1 + ms = Search.new_musicians(@user1, Time.now - 1.week) + ms.should_not be_nil + ms.length.should == 3 + ms.should eq [@user2, @user3, @user4] end - it "sends new musician email" do - # create new user outside 500 from Apex to ensure its excluded from results - FactoryGirl.create(:user, {city: "Austin", state: "TX", country: "US"}) - User.geocoded_users.find_each do |usr| - Search.new_musicians(usr) do |new_usrs| - # the newly created user is not nearby the existing users (which are in Apex, NC) - # and that user is not included in query - expect(new_usrs.count).to eq(User.musicians.count - 1) - end - end + it "find two for user2" do + # user1,3,4 are scored against user1, but user4 is bad + ms = Search.new_musicians(@user2, Time.now - 1.week) + ms.should_not be_nil + ms.length.should == 2 + ms.should eq [@user3, @user1] + end + + it "find two for user3" do + # user1..2 are scored against user3 + ms = Search.new_musicians(@user3, Time.now - 1.week) + ms.should_not be_nil + ms.length.should == 2 + ms.should eq [@user2, @user1] + end + + it "find one for user4" do + # user1..2 are scored against user4, but user2 is bad + ms = Search.new_musicians(@user4, Time.now - 1.week) + ms.should_not be_nil + ms.length.should == 1 + ms.should eq [@user1] + end + + it "find none for user5" do + # user1..4 are not scored against user5 + ms = Search.new_musicians(@user5, Time.now - 1.week) + ms.should_not be_nil + ms.length.should == 0 end end diff --git a/ruby/spec/jam_ruby/models/recording_spec.rb b/ruby/spec/jam_ruby/models/recording_spec.rb index f3966feb5..fa666cbea 100644 --- a/ruby/spec/jam_ruby/models/recording_spec.rb +++ b/ruby/spec/jam_ruby/models/recording_spec.rb @@ -80,7 +80,7 @@ describe Recording do @track2 = FactoryGirl.create(:track, :connection => @connection2, :instrument => @instrument2) # @music_session.connections << @connection2 - @connection2.join_the_session(@music_session, true, nil) + @connection2.join_the_session(@music_session, true, nil, @user2) @recording = Recording.start(@music_session, @user) @user.recordings.length.should == 0 @@ -179,7 +179,7 @@ describe Recording do @track = FactoryGirl.create(:track, :connection => @connection2, :instrument => @instrument) # @music_session.connections << @connection2 @music_session.save - @connection2.join_the_session(@music_session, true, nil) + @connection2.join_the_session(@music_session, true, nil, @user2) @recording = Recording.start(@music_session, @user) @recording.stop @recording.reload diff --git a/ruby/spec/jam_ruby/models/score_spec.rb b/ruby/spec/jam_ruby/models/score_spec.rb index 84efb6127..65e806d6c 100644 --- a/ruby/spec/jam_ruby/models/score_spec.rb +++ b/ruby/spec/jam_ruby/models/score_spec.rb @@ -4,7 +4,7 @@ describe Score do before do Score.delete_all - Score.createx(1234, 'anodeid', 0x01020304, 2345, 'bnodeid', 0x02030405, 20, nil) + Score.createx(1234, 'anodeid', 0x01020304, 2345, 'bnodeid', 0x02030405, 20, nil, 'foo') Score.createx(1234, 'anodeid', 0x01020304, 3456, 'cnodeid', 0x03040506, 30, nil) Score.createx(1234, 'anodeid', 0x01020304, 3456, 'cnodeid', 0x03040506, 40, Time.new.utc-3600) end @@ -25,6 +25,7 @@ describe Score do s.score.should == 20 s.scorer.should == 0 s.score_dt.should_not be_nil + s.scoring_data.should eq('foo') end it 'b to a' do @@ -39,6 +40,7 @@ describe Score do s.score.should == 20 s.scorer.should == 1 s.score_dt.should_not be_nil + s.scoring_data.should be_nil end it 'a to c' do @@ -53,6 +55,7 @@ describe Score do s.score.should == 30 s.scorer.should == 0 s.score_dt.should_not be_nil + s.scoring_data.should be_nil end it 'c to a' do @@ -67,6 +70,7 @@ describe Score do s.score.should == 30 s.scorer.should == 1 s.score_dt.should_not be_nil + s.scoring_data.should be_nil end it 'delete a to c' do @@ -92,4 +96,31 @@ describe Score do Score.findx(3456, 3456).should == -1 end + it "test shortcut for making scores from connections" do + user1 = FactoryGirl.create(:user) + conn1 = FactoryGirl.create(:connection, user: user1, addr: 0x01020304, locidispid: 5) + user2 = FactoryGirl.create(:user) + conn2 = FactoryGirl.create(:connection, user: user2, addr: 0x11121314, locidispid: 6) + user3 = FactoryGirl.create(:user) + conn3 = FactoryGirl.create(:connection, user: user3, addr: 0x21222324, locidispid: 7) + + Score.findx(5, 6).should == -1 + Score.findx(6, 5).should == -1 + Score.findx(5, 7).should == -1 + Score.findx(7, 5).should == -1 + Score.findx(6, 7).should == -1 + Score.findx(7, 6).should == -1 + + Score.score_conns(conn1, conn2, 12) + Score.score_conns(conn1, conn3, 13) + Score.score_conns(conn2, conn3, 23) + + Score.findx(5, 6).should == 12 + Score.findx(6, 5).should == 12 + Score.findx(5, 7).should == 13 + Score.findx(7, 5).should == 13 + Score.findx(6, 7).should == 23 + Score.findx(7, 6).should == 23 + end + end diff --git a/ruby/spec/jam_ruby/models/user_location_spec.rb b/ruby/spec/jam_ruby/models/user_location_spec.rb index b900f9b98..6a0ad602a 100644 --- a/ruby/spec/jam_ruby/models/user_location_spec.rb +++ b/ruby/spec/jam_ruby/models/user_location_spec.rb @@ -10,13 +10,13 @@ X If no profile location is provided, and the user creates/joins a music session =end before do - @geocode1 = FactoryGirl.create(:geocoder) - @geocode2 = FactoryGirl.create(:geocoder) - @user = User.new(first_name: "Example", last_name: "User", email: "user@example.com", - password: "foobar", password_confirmation: "foobar", - city: "Apex", state: "NC", country: "US", - terms_of_service: true, musician: true) - @user.save! + # @geocode1 = FactoryGirl.create(:geocoder) + # @geocode2 = FactoryGirl.create(:geocoder) + # @user = User.new(first_name: "Example", last_name: "User", email: "user@example.com", + # password: "foobar", password_confirmation: "foobar", + # city: "Apex", state: "NC", country: "US", + # terms_of_service: true, musician: true) + # @user.save! end describe "with profile location data" do diff --git a/ruby/spec/jam_ruby/resque/music_session_scheduler_spec.rb b/ruby/spec/jam_ruby/resque/music_session_scheduler_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/ruby/spec/mailers/user_mailer_spec.rb b/ruby/spec/mailers/user_mailer_spec.rb index 30ecc52af..c6cd4db5b 100644 --- a/ruby/spec/mailers/user_mailer_spec.rb +++ b/ruby/spec/mailers/user_mailer_spec.rb @@ -137,23 +137,23 @@ describe UserMailer do end - describe "sends new musicians email" do + # describe "sends new musicians email" do - let(:mail) { UserMailer.deliveries[0] } + # let(:mail) { UserMailer.deliveries[0] } - before(:each) do - UserMailer.new_musicians(user, User.musicians).deliver - end + # before(:each) do + # UserMailer.new_musicians(user, User.musicians).deliver + # end - it { UserMailer.deliveries.length.should == 1 } + # it { UserMailer.deliveries.length.should == 1 } - it { mail['from'].to_s.should == UserMailer::DEFAULT_SENDER } - it { mail['to'].to_s.should == user.email } - it { mail.multipart?.should == true } # because we send plain + html + # it { mail['from'].to_s.should == UserMailer::DEFAULT_SENDER } + # it { mail['to'].to_s.should == user.email } + # it { mail.multipart?.should == true } # because we send plain + html - # verify that the messages are correctly configured - it { mail.html_part.body.include?("New JamKazam Musicians in your Area").should be_true } - it { mail.text_part.body.include?("New JamKazam Musicians in your Area").should be_true } - end + # # verify that the messages are correctly configured + # it { mail.html_part.body.include?("New JamKazam Musicians in your Area").should be_true } + # it { mail.text_part.body.include?("New JamKazam Musicians in your Area").should be_true } + # end end diff --git a/ruby/spec/spec_db.rb b/ruby/spec/spec_db.rb index b825973f5..184b6f4c1 100644 --- a/ruby/spec/spec_db.rb +++ b/ruby/spec/spec_db.rb @@ -6,7 +6,13 @@ class SpecDb def self.recreate_database conn = PG::Connection.open("dbname=postgres user=postgres password=postgres host=localhost") conn.exec("DROP DATABASE IF EXISTS #{TEST_DB_NAME}") - conn.exec("CREATE DATABASE #{TEST_DB_NAME}") + if ENV['TABLESPACE'] + conn.exec("CREATE DATABASE #{TEST_DB_NAME} WITH TABLESPACE=#{ENV['TABLESPACE']}") + else + conn.exec("CREATE DATABASE #{TEST_DB_NAME}") + end + + JamDb::Migrator.new.migrate(:dbname => TEST_DB_NAME, :user => "postgres", :password => "postgres", :host => "localhost") end end diff --git a/ruby/spec/spec_helper.rb b/ruby/spec/spec_helper.rb index c29b007e6..faaeab0b8 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -3,6 +3,7 @@ ENV["RAILS_ENV"] = "test" require 'simplecov' require 'support/utilities' +require 'support/profile' require 'active_record' require 'jam_db' require 'spec_db' @@ -30,7 +31,7 @@ require 'timecop' require 'resque_spec/scheduler' # uncomment this to see active record logs -#ActiveRecord::Base.logger = Logger.new(STDOUT) if defined?(ActiveRecord::Base) +# ActiveRecord::Base.logger = Logger.new(STDOUT) if defined?(ActiveRecord::Base) include JamRuby diff --git a/ruby/spec/support/profile.rb b/ruby/spec/support/profile.rb new file mode 100644 index 000000000..60eab2a83 --- /dev/null +++ b/ruby/spec/support/profile.rb @@ -0,0 +1,22 @@ +if ENV['PROFILE'] + require 'ruby-prof' + RSpec.configure do |c| + def profile + result = RubyProf.profile { yield } + name = example.metadata[:full_description].downcase.gsub(/[^a-z0-9_-]/, "-").gsub(/-+/, "-") + printer = RubyProf::CallTreePrinter.new(result) + Dir.mkdir('tmp/performance') + open("tmp/performance/callgrind.#{name}.#{Time.now.to_i}.trace", "w") do |f| + printer.print(f) + end + end + + c.around(:each) do |example| + if ENV['PROFILE'] == 'all' or (example.metadata[:profile] and ENV['PROFILE']) + profile { example.run } + else + example.run + end + end + end +end diff --git a/web/Gemfile b/web/Gemfile index 49876c9f2..e27ca71d1 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -5,7 +5,7 @@ unless ENV["LOCAL_DEV"] == "1" end # Look for $WORKSPACE, otherwise use "workspace" as dev path. -devenv = ENV["BUILD_NUMBER"].nil? || ENV["TEST_WWW"] == "1" +devenv = ENV["BUILD_NUMBER"].nil? # Jenkins sets a build number environment variable if devenv gem 'jam_db', :path=> "../db/target/ruby_package" @@ -71,7 +71,7 @@ gem 'resque_mailer' #gem 'typescript-src', path: '../../typescript-src-ruby' #gem 'typescript-node', path: '../../typescript-node-ruby' #gem 'typescript-rails', path: '../../typescript-rails' - +gem 'netaddr' gem 'quiet_assets', :group => :development gem 'bugsnag' gem 'multi_json', '1.9.0' @@ -132,4 +132,4 @@ end group :package do gem 'fpm' -end \ No newline at end of file +end diff --git a/web/app/assets/images/content/icon_blue_score.png b/web/app/assets/images/content/icon_blue_score.png new file mode 100644 index 000000000..fa7e54fc0 Binary files /dev/null and b/web/app/assets/images/content/icon_blue_score.png differ diff --git a/web/app/assets/images/content/icon_green_score.png b/web/app/assets/images/content/icon_green_score.png new file mode 100644 index 000000000..13b89c4f7 Binary files /dev/null and b/web/app/assets/images/content/icon_green_score.png differ diff --git a/web/app/assets/images/content/icon_purple_score.png b/web/app/assets/images/content/icon_purple_score.png new file mode 100644 index 000000000..6374e3456 Binary files /dev/null and b/web/app/assets/images/content/icon_purple_score.png differ diff --git a/web/app/assets/images/content/icon_red_score.png b/web/app/assets/images/content/icon_red_score.png new file mode 100644 index 000000000..64e916388 Binary files /dev/null and b/web/app/assets/images/content/icon_red_score.png differ diff --git a/web/app/assets/images/content/icon_yellow_score.png b/web/app/assets/images/content/icon_yellow_score.png new file mode 100644 index 000000000..3794c498e Binary files /dev/null and b/web/app/assets/images/content/icon_yellow_score.png differ diff --git a/web/app/assets/javascripts/AAA_Log.js b/web/app/assets/javascripts/AAA_Log.js index bdd411e38..90851d08a 100644 --- a/web/app/assets/javascripts/AAA_Log.js +++ b/web/app/assets/javascripts/AAA_Log.js @@ -19,6 +19,14 @@ 'log':null, 'debug':null, 'info':null, 'warn':null, 'error':null, 'assert':null, 'trace':null, 'exception':null } + var backend_methods = { + "log" : 4, + "debug" : 4, + "info" : 3, + "warn" : 2, + "error" : 1 + } + var logCache = []; if ('undefined' === typeof(context.console)) { @@ -33,15 +41,28 @@ context.console.debug = function() { console.log(arguments); } } + console.proxy_logs_to_backend = false; + // http://tobyho.com/2012/07/27/taking-over-console-log/ function takeOverConsole(){ var console = window.console - if (!console) return + if (!console) return; + var i = null; function intercept(method){ var original = console[method] console[method] = function(){ - logCache.push([method].concat(arguments)); + var logAsString = []; + for(i in arguments) { + var arg = arguments[i]; + try { + logAsString.push(JSON.stringify(arg)); + } + catch(e) { + logAsString.push("unable to parse node: " + e.toString()); + } + } + logCache.push([method].concat(logAsString)); if(logCache.length > 50) { // keep the cache size 50 or lower logCache.pop(); @@ -55,9 +76,15 @@ var message = Array.prototype.slice.apply(arguments).join(' ') original(message) } + if(console.proxy_logs_to_backend && context.jamClient) { + var backendLevel = backend_methods[method]; + if(backendLevel) { + context.jamClient.log(backendLevel, logAsString.join(', ')); + } + } } } - var methods = ['log', 'warn', 'error'] + var methods = ['log', 'warn', 'error', 'debug', 'info'] for (var i = 0; i < methods.length; i++) intercept(methods[i]) } diff --git a/web/app/assets/javascripts/AAB_message_factory.js b/web/app/assets/javascripts/AAB_message_factory.js index 4108ea118..2959e8485 100644 --- a/web/app/assets/javascripts/AAB_message_factory.js +++ b/web/app/assets/javascripts/AAB_message_factory.js @@ -135,6 +135,18 @@ return client_container(msg.LOGIN, route_to.SERVER, login); }; + // create a login message using only the client_id. only valid for latency_tester + factory.login_with_client_id = function(client_id, client_type) { + if(client_type != 'latency_tester') { + throw "client_type must be latency_tester in login_with_client_id"; + } + var login = { + client_id : client_id, + client_type : client_type + }; + return client_container(msg.LOGIN, route_to.SERVER, login); + }; + // create a music session login message factory.login_music_session = function(music_session) { var login_music_session = { music_session : music_session }; diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index 44321fd23..c61b8b6fe 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -19,6 +19,7 @@ // uniquely identify the websocket connection var channelId = null; var clientType = null; + var mode = null; // heartbeat var heartbeatInterval = null; @@ -156,10 +157,11 @@ // for debugging purposes, see if the last time we've sent a heartbeat is way off (500ms) of the target interval var now = new Date(); - if (lastHeartbeatSentTime) { + + if(lastHeartbeatSentTime) { var drift = new Date().getTime() - lastHeartbeatSentTime.getTime() - heartbeatMS; if (drift > 500) { - logger.error("significant drift between heartbeats: " + drift + 'ms beyond target interval') + logger.warn("significant drift between heartbeats: " + drift + 'ms beyond target interval') } } lastHeartbeatSentTime = now; @@ -168,6 +170,10 @@ } } + function isClientMode() { + return mode == "client"; + } + function loggedIn(header, payload) { if (!connectTimeout) { @@ -179,21 +185,21 @@ app.clientId = payload.client_id; - // tell the backend that we have logged in - context.jamClient.OnLoggedIn(payload.user_id, payload.token); - - $.cookie('client_id', payload.client_id); - + if(isClientMode()) { + // tell the backend that we have logged in + context.jamClient.OnLoggedIn(payload.user_id, payload.token); + $.cookie('client_id', payload.client_id); + } heartbeatMS = payload.heartbeat_interval * 1000; connection_expire_time = payload.connection_expire_time * 1000; - logger.debug("jamkazam.js.loggedIn(): clientId=" + app.clientId + ", heartbeat=" + payload.heartbeat_interval + "s, expire_time=" + payload.connection_expire_time + 's'); + logger.info("jamkazam.js.loggedIn(): clientId=" + app.clientId + ", heartbeat=" + payload.heartbeat_interval + "s, expire_time=" + payload.connection_expire_time + 's'); heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS); heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000); lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat connectDeferred.resolve(); - activeElementEvent('afterConnect', payload); + activeElementEvent('afterConnect', payload); } function heartbeatAck(header, payload) { @@ -264,7 +270,7 @@ .always(function() { if ($currentDisplay.is('.no-websocket-connection')) { // this path is the 'not in session path'; so there is nothing else to do - $currentDisplay.hide(); + $currentDisplay.removeClass('active'); // TODO: tell certain elements that we've reconnected } @@ -298,7 +304,7 @@ $inSituContent.find('.reconnect-countdown').html(formatDelaySecs(reconnectDelaySecs())); $messageContents.empty(); $messageContents.append($inSituContent); - $inSituBannerHolder.show(); + $inSituBannerHolder.addClass('active'); content = $inSituBannerHolder; } @@ -446,6 +452,10 @@ if(!clientType) { clientType = context.JK.clientType(); } + if(!mode) { + mode = context.jamClient.getOperatingMode(); + } + connectDeferred = new $.Deferred(); channelId = context.JK.generateUUID(); // create a new channel ID for every websocket connection @@ -460,7 +470,8 @@ connectTimeout = setTimeout(function () { connectTimeout = null; - if (connectDeferred.state() === 'pending') { + + if(connectDeferred.state() === 'pending') { server.close(true); connectDeferred.reject(); } @@ -470,7 +481,7 @@ }; server.close = function (in_error) { - logger.log("closing websocket"); + logger.info("closing websocket"); clientClosedConnection = true; server.socket.close(); @@ -486,9 +497,20 @@ server.send(loginMessage); }; + server.latencyTesterLogin = function() { + var loginMessage = msg_factory.login_with_client_id(context.jamClient.clientID, 'latency_tester'); + server.send(loginMessage); + } + server.onOpen = function () { - logger.log("server.onOpen"); - server.rememberLogin(); + logger.debug("server.onOpen"); + + if(isClientMode()) { + server.rememberLogin(); + } + else { + server.latencyTesterLogin(); + } }; server.onMessage = function (e) { @@ -498,7 +520,7 @@ callbacks = server.dispatchTable[message.type]; if (message.type != context.JK.MessageType.HEARTBEAT_ACK && message.type != context.JK.MessageType.PEER_MESSAGE) { - logger.log("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload)); + logger.info("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload)); } if (callbacks !== undefined) { @@ -513,13 +535,13 @@ } } else { - logger.log("Unexpected message type %s.", message.type); + logger.info("Unexpected message type %s.", message.type); } }; // onClose is called if either client or server closes connection server.onClose = function () { - logger.log("Socket to server closed."); + logger.info("Socket to server closed."); if (connectDeferred.state() === "pending") { connectDeferred.reject(); @@ -533,12 +555,12 @@ var jsMessage = JSON.stringify(message); if (message.type != context.JK.MessageType.HEARTBEAT && message.type != context.JK.MessageType.PEER_MESSAGE) { - logger.log("server.send(" + jsMessage + ")"); + logger.info("server.send(" + jsMessage + ")"); } if (server !== undefined && server.socket !== undefined && server.socket.send !== undefined) { server.socket.send(jsMessage); } else { - logger.log("Dropped message because server connection is closed."); + logger.warn("Dropped message because server connection is closed."); } }; @@ -546,7 +568,7 @@ var loginMessage; if (!server.signedIn) { - logger.log("Not signed in!"); + logger.warn("Not signed in!"); // TODO: surface the error return; } @@ -599,7 +621,7 @@ server.connected = true; if (context.jamClient !== undefined) { - logger.debug("... (handling LOGIN_ACK) Updating backend client, connected to true and clientID to " + + logger.info("... (handling LOGIN_ACK) Updating backend client, connected to true and clientID to " + payload.client_id); context.jamClient.connected = true; context.jamClient.clientID = server.clientID; diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js index b346b467c..046a09772 100644 --- a/web/app/assets/javascripts/accounts.js +++ b/web/app/assets/javascripts/accounts.js @@ -29,16 +29,19 @@ var validProfiles = prettyPrintAudioProfiles(context.JK.getGoodConfigMap()); var invalidProfiles = prettyPrintAudioProfiles(context.JK.getBadConfigMap()); - var template = context.JK.fillTemplate($('#template-account-main').html(), { - email: userDetail.email, - name: userDetail.name, - location : userDetail.location, - instruments : prettyPrintInstruments(userDetail.instruments), - photoUrl : context.JK.resolveAvatarUrl(userDetail.photo_url), - validProfiles : validProfiles, - invalidProfiles : invalidProfiles - }); - $('#account-content-scroller').html(template); + var $template = $(context._.template($('#template-account-main').html(), { + email: userDetail.email, + name: userDetail.name, + location : userDetail.location, + instruments : prettyPrintInstruments(userDetail.instruments), + photoUrl : context.JK.resolveAvatarUrl(userDetail.photo_url), + validProfiles : validProfiles, + invalidProfiles : invalidProfiles, + isNativeClient: gon.isNativeClient, + musician: context.JK.currentUserMusician + } , { variable: 'data' })); + + $('#account-content-scroller').html($template); } function prettyPrintAudioProfiles(profileMap) { diff --git a/web/app/assets/javascripts/accounts_audio_profile.js b/web/app/assets/javascripts/accounts_audio_profile.js index f76e1f08b..3b1b7ee60 100644 --- a/web/app/assets/javascripts/accounts_audio_profile.js +++ b/web/app/assets/javascripts/accounts_audio_profile.js @@ -35,18 +35,30 @@ } function populateAccountAudio() { + var all = context.jamClient.FTUEGetAllAudioConfigurations(); + var good = context.jamClient.FTUEGetGoodAudioConfigurations(); + var current = context.jamClient.FTUEGetMusicProfileName(); - // load Audio Driver dropdown - var devices = context.jamClient.TrackGetDevices(); + var profiles = []; + context._.each(all, function(item) { + profiles.push({id: item, good: false, class:'bad', current: current == item, active_text: current == item ? '(active)' : ''}) + }); - var options = { - devices: devices + if(good) { + for(var i = 0; i < good.length; i++) { + for(var j = 0; j < profiles.length; j++) { + if(good[i] == profiles[j].id) { + profiles[j].good = true; + profiles[j].class = 'good'; + break; + } + } + } } - var template = context._.template($('#template-account-audio').html(), options, {variable: 'data'}); + var template = context._.template($('#template-account-audio').html(), {profiles: profiles}, {variable: 'data'}); appendAudio(template); - } function appendAudio(template) { @@ -65,9 +77,29 @@ populateAccountAudio(); } + function handleActivateAudioProfile(audioProfileId) { + logger.debug("activating audio profile: " + audioProfileId); + + if(audioProfileId == context.jamClient.FTUEGetMusicProfileName()) { + context.JK.Banner.showAlert("This profile is already active."); + return; + } + + var result = context.jamClient.FTUELoadAudioConfiguration(audioProfileId); + + if(!result) { + logger.error("unable to activate audio configuration: " + audioProfileId); + context.JK.alertSupportedNeeded("Unable to activate audio configuration for profile named: " + audioProfileId); + } + + // redraw after activation of profile + populateAccountAudio(); + } function handleStartAudioQualification() { if(true) { + app.afterFtue = function() { populateAccountAudio() }; + app.cancelFtue = function() { populateAccountAudio() }; app.layout.startNewFtue(); } else { @@ -91,12 +123,27 @@ // events for main screen function events() { // wire up main panel clicks - $('#account-audio-content-scroller').on('click', 'a[data-purpose=delete-audio-profile]', function (evt) { + var $root = $('#account-audio-content-scroller'); + $root.on('click', 'a[data-purpose=delete-audio-profile]', function (evt) { evt.stopPropagation(); handleDeleteAudioProfile($(this).attr('data-id')); return false; }); - $('#account-audio-content-scroller').on('click', 'a[data-purpose=add-profile]', function (evt) { + + $root.on('click', 'a[data-purpose=activate-audio-profile]', function (evt) { + evt.stopPropagation(); + var $btn = $(this); + var status = $btn.closest('tr').attr('data-status'); + if(status == "good") { + handleActivateAudioProfile($btn.attr('data-id')); + } + else { + context.JK.Banner.showAlert("Unable to activate this profile. Please verify that the devices associated are connected."); + } + return false; + }); + + $root.on('click', 'a[data-purpose=add-profile]', function (evt) { evt.stopPropagation(); handleStartAudioQualification(); return false; diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index 8f71eeb69..6aad0085d 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -15,6 +15,7 @@ //= require jquery_ujs //= require jquery.ui.datepicker //= require jquery.ui.draggable +//= require jquery.ui.droppable //= require jquery.bt //= require jquery.icheck //= require jquery.color @@ -41,3 +42,6 @@ //= require custom_controls //= require_directory ./scheduled_session //= require_directory . +//= require_directory ./wizard +//= require_directory ./wizard/gear +//= require_directory ./wizard/loopback diff --git a/web/app/assets/javascripts/bandProfile.js b/web/app/assets/javascripts/bandProfile.js index 6812e751e..46ed0b72c 100644 --- a/web/app/assets/javascripts/bandProfile.js +++ b/web/app/assets/javascripts/bandProfile.js @@ -344,7 +344,7 @@ var instrument = musician.instruments[j]; var inst = '../assets/content/icon_instrument_default24.png'; if (instrument.instrument_id in instrument_logo_map) { - inst = instrument_logo_map[instrument.instrument_id]; + inst = instrument_logo_map[instrument.instrument_id].asset; } instrumentLogoHtml += ' '; } diff --git a/web/app/assets/javascripts/banner.js b/web/app/assets/javascripts/banner.js index 7b11a3211..5bb003440 100644 --- a/web/app/assets/javascripts/banner.js +++ b/web/app/assets/javascripts/banner.js @@ -8,7 +8,7 @@ context.JK.Banner = (function () { var self = this; var logger = context.JK.logger; - var $banner = $('#banner'); + var $banner = null; function showAlert(options) { if (typeof options == 'string' || options instanceof String) { @@ -35,13 +35,33 @@ throw "unable to show banner for empty message"; } - if(options.type == "alert" || options.close) { - var $closeBtn = $('#banner').find('.close-btn'); + var $closeBtn = $banner.find('.close-btn'); - $closeBtn.click(function() { + if((options.type == "alert" && !options.buttons) || options.close) { + + $closeBtn.show().click(function() { hide(); return false; - }).show(); + }); + } + else { + $closeBtn.hide(); + } + + if(options.buttons) { + var $buttons = $banner.find('.buttons') + context._.each(options.buttons, function(button) { + if(!button.name) throw "button.name must be specified"; + if(!button.click) throw "button.click must be specified"; + + var $btn = $('' + button.name + ''); + $btn.click(function() { + button.click(); + hide(); + return false; + }); + $buttons.append($btn); + }); } $('#banner').attr('data-type', options.type).show() @@ -52,13 +72,15 @@ } function hide() { - $('#banner').hide(); + $banner.hide(); + $banner.find('.user-btn').remove(); $('#banner_overlay .dialog-inner').html(""); $('#banner_overlay').hide(); } function initialize() { + $banner = $('#banner'); return self; } diff --git a/web/app/assets/javascripts/chatPanel.js b/web/app/assets/javascripts/chatPanel.js index fef16ab0b..7d3f995fc 100644 --- a/web/app/assets/javascripts/chatPanel.js +++ b/web/app/assets/javascripts/chatPanel.js @@ -139,14 +139,14 @@ $form.submit(sendMessage); $textBox.keydown(handleEnter); $sendChatMessageBtn.click(sendMessage); - - registerChatMessage(bind); } else { $form.submit(null); $textBox.keydown(null); $sendChatMessageBtn.click(null); } + + registerChatMessage(bind); } // called from sidebar when messages come in @@ -338,7 +338,7 @@ this.initialize = initialize; this.sessionStarted = sessionStarted; - this.sessionStopped = sessionStopped; + this.sessionStopped = sessionStopped; this.registerChatMessage = registerChatMessage; }; diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index af10cb53b..0f580bca0 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -38,7 +38,15 @@ function RestartApplication() { } + function FTUECancel() { + } + function FTUEGetMusicProfileName() { + return "default" + } + function FTUESetMusicProfileName() { + + } function FTUEGetInputLatency() { dbg("FTUEGetInputLatency"); return 2; @@ -85,6 +93,9 @@ function FTUEGetStatus() { return ftueStatus; } function FTUESetStatus(b) { ftueStatus = b; } function FTUESetMusicDevice(id) { dbg("FTUESetMusicDevice"); } + function FTUEGetAudioDevices() { + return {"devices":[{"display_name":"Built-in","guid":"Built-in","input_count":1,"name":"Built-in","output_count":1,"port_audio_name":"Built-in"},{"display_name":"JamKazam Virtual Monitor","guid":"JamKazam Virtual Monitor","input_count":0,"name":"JamKazam Virtual Monitor","output_count":1,"port_audio_name":"JamKazam Virtual Monitor"}]} + } function FTUEGetDevices() { dbg('FTUEGetMusicDevices'); return { @@ -113,6 +124,88 @@ "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" }; } + function FTUEGetChannels() { + return { + "inputs": [ + { + "assignment": 1, + "chat": false, + "device_id": "Built-in Microph", + "device_type": 5, + "id": "i~5~Built-in Microph~0~0~Built-in", + "name": "Built-in Microph - Left", + "number": 0 + }, + { + "assignment": 0, + "chat": false, + "device_id": "Built-in Microph", + "device_type": 5, + "id": "i~5~Built-in Microph~1~0~Built-in", + "name": "Built-in Microph - Right", + "number": 1 + } + ], + "outputs": [ + { + "assignment": -1, + "chat": false, + "device_id": "Built-in Output", + "device_type": 5, + "id": "o~5~Built-in Output~0~0~Built-in", + "name": "Built-in Output - Left", + "number": 0 + }, + { + "assignment": -1, + "chat": false, + "device_id": "Built-in Output", + "device_type": 5, + "id": "o~5~Built-in Output~1~0~Built-in", + "name": "Built-in Output - Right", + "number": 1 + } + ] + }; + } + function FTUEGetAudioDevices() { + return { + "devices": [ + { + "display_name": "Built-in", + "guid": "Built-in", + "input_count": 1, + "name": "Built-in", + "output_count": 1, + "port_audio_name": "Built-in" + }, + { + "display_name": "JamKazam Virtual Monitor", + "guid": "JamKazam Virtual Monitor", + "input_count": 0, + "name": "JamKazam Virtual Monitor", + "output_count": 1, + "port_audio_name": "JamKazam Virtual Monitor" + } + ] + }; + } + function FTUEStartIoPerfTest() {} + function FTUEGetIoPerfData() { + return { + "in_var" : 0.15, + "out_var" : 0.25, + "in_median" : 399.9, + "out_median" : 400.3, + "in_target" : 400, + "out_target" : 400 + }; + } + function FTUESetInputMusicDevice() { } + function FTUESetOutputMusicDevice() { } + function FTUEGetInputMusicDevice() { return 'Built-in'; } + function FTUEGetOutputMusicDevice() { return 'Built-in'; } + function FTUESetMusicInput() { dbg('FTUESetMusicInput'); } function FTUESetChatInput() { dbg('FTUESetChatInput'); } function FTUESetMusicOutput() { dbg('FTUESetMusicOutput'); } @@ -250,6 +343,19 @@ eval(js); } + function IsMyNetworkWireless() { + // 1=true, 0 = false, -1=unknown + return 1; + } + + function SetNetworkTestScore(numClients) { + + } + + function GetNetworkTestScore() { + return 8; + } + function GetASIODevices() { var response =[{"device_id":0,"device_name":"Realtek High Definition Audio","device_type": 0,"interfaces":[{"interface_id":0,"interface_name":"Realtek HDA SPDIF Out","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":1,"interface_name":"Realtek HD Audio rear output","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":2,"interface_name":"Realtek HD Audio Mic input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":3,"interface_name":"Realtek HD Audio Line input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":4,"interface_name":"Realtek HD Digital input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"}]},{"interface_id":5,"interface_name":"Realtek HD Audio Stereo input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]}],"wavert_supported":false},{"device_id":1,"device_name":"M-Audio FW Audiophile","device_type": 1,"interfaces":[{"interface_id":0,"interface_name":"FW AP Multi","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":1,"interface_name":"FW AP 1/2","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":2,"interface_name":"FW AP SPDIF","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":3,"interface_name":"FW AP 3/4","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":2,"device_name":"Virtual Audio Cable","device_type": 2,"interfaces":[{"interface_id":0,"interface_name":"Virtual Cable 2","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]},{"interface_id":1,"interface_name":"Virtual Cable 1","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":3,"device_name":"WebCamDV WDM Audio Capture","device_type": 3,"interfaces":[{"interface_id":0,"interface_name":"WebCamDV Audio","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"},{"is_input":false,"pin_id":1,"pin_name":"Volume Control"}]}],"wavert_supported":false}]; return response; @@ -351,6 +457,8 @@ function SessionStartRecording() {} function SessionStopPlay() {} function SessionStopRecording() {} + function SessionAddPlayTrack() {return true;} + function SessionRemoveAllPlayTracks(){} function isSessionTrackPlaying() { return false; } function SessionCurrrentPlayPosMs() { return 0; } function SessionGetTracksPlayDurationMs() { return 0; } @@ -580,16 +688,51 @@ fakeJamClientRecordings = fakeRecordingsImpl; } - function OnLoggedIn(userId, sessionToken) { + function TestNetworkPktBwRate(targetClientId, successCallback, timeoutCallback, testType, duration, numClients, payloadSize) { + var progress = {progress:true, + upthroughput:.95, + downthroughput:.95, + upjitter: 2.3, + downjitter: 2.3 + } + var count = 0; + var interval = setInterval(function() { + eval(successCallback + "(" + JSON.stringify(progress) + ");"); + + if(progress.upthroughput < 1) { + progress.upthroughput += .05; + } + if(progress.downthroughput < 1) { + progress.downthroughput += .05; + } + + count++; + if(count == duration) { + clearInterval(interval); + + delete progress['progress'] + progress.pass = true; + eval(successCallback + "(" + JSON.stringify(progress) + ");"); + } + }, 1000); + } + function StopNetworkTest(targetClientId) {} + function OnLoggedIn(userId, sessionToken) {} + function OnLoggedOut() {} + function UserAttention(option) {} + + function log(level, message) { + console.log("beep : " + message) } - function OnLoggedOut() { - - } - - function UserAttention(option) { - + function getOperatingMode() { + if (location.pathname == '/latency_tester') { + return 'server'; + } + else { + return 'client'; + } } // passed an array of recording objects from the server @@ -653,13 +796,27 @@ this.StopRecording = StopRecording; this.TestASIOLatency = TestASIOLatency; this.TestLatency = TestLatency; + this.IsMyNetworkWireless = IsMyNetworkWireless; + this.SetNetworkTestScore = SetNetworkTestScore; + this.GetNetworkTestScore = GetNetworkTestScore; this.connected = true; // FTUE (round 3) + this.FTUESetInputMusicDevice = FTUESetInputMusicDevice; + this.FTUESetOutputMusicDevice = FTUESetOutputMusicDevice; + this.FTUEGetInputMusicDevice = FTUEGetInputMusicDevice; + this.FTUEGetOutputMusicDevice = FTUEGetOutputMusicDevice; this.FTUEGetChatInputVolume = FTUEGetChatInputVolume; this.FTUEGetChatInputs = FTUEGetChatInputs; + this.FTUEGetChannels = FTUEGetChannels; + this.FTUEGetAudioDevices = FTUEGetAudioDevices; + this.FTUEStartIoPerfTest = FTUEStartIoPerfTest; + this.FTUEGetIoPerfData = FTUEGetIoPerfData; this.FTUEGetDevices = FTUEGetDevices; this.FTUEGetFrameSize = FTUEGetFrameSize; + this.FTUECancel = FTUECancel; + this.FTUEGetMusicProfileName = FTUEGetMusicProfileName; + this.FTUESetMusicProfileName = FTUESetMusicProfileName; this.FTUEGetInputLatency = FTUEGetInputLatency; this.FTUEGetInputVolume = FTUEGetInputVolume; this.FTUEGetMusicInputs = FTUEGetMusicInputs; @@ -708,6 +865,8 @@ this.SessionStartPlay = SessionStartPlay; this.SessionStartRecording = SessionStartRecording; this.SessionStopPlay = SessionStopPlay; + this.SessionAddPlayTrack = SessionAddPlayTrack; + this.SessionRemoveAllPlayTracks = SessionRemoveAllPlayTracks; this.SessionStopRecording = SessionStopRecording; this.isSessionTrackPlaying = isSessionTrackPlaying; this.SessionCurrrentPlayPosMs = SessionCurrrentPlayPosMs; @@ -780,6 +939,13 @@ // fake calls; not a part of the actual jam client this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks; this.SetFakeRecordingImpl = SetFakeRecordingImpl; + + // network test + this.TestNetworkPktBwRate = TestNetworkPktBwRate; + this.StopNetworkTest = StopNetworkTest; + this.log = log; + this.getOperatingMode = getOperatingMode; + this.clientID = "devtester"; }; })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/fakeJamClientRecordings.js b/web/app/assets/javascripts/fakeJamClientRecordings.js index 5fd412e0b..9fee4360f 100644 --- a/web/app/assets/javascripts/fakeJamClientRecordings.js +++ b/web/app/assets/javascripts/fakeJamClientRecordings.js @@ -34,8 +34,8 @@ function StartRecording(recordingId, clients) { startingSessionState = {}; - // we expect all clients to respond within 3 seconds to mimic the reliable UDP layer - startingSessionState.aggegratingStartResultsTimer = setTimeout(timeoutStartRecordingTimer, 3000); + // we expect all clients to respond within 1 seconds to mimic the reliable UDP layer + startingSessionState.aggegratingStartResultsTimer = setTimeout(timeoutStartRecordingTimer, 1000); startingSessionState.recordingId = recordingId; startingSessionState.groupedClientTracks = copyClientIds(clients, app.clientId); // we will manipulate this new one @@ -70,8 +70,8 @@ stoppingSessionState = {}; - // we expect all clients to respond within 3 seconds to mimic the reliable UDP layer - stoppingSessionState.aggegratingStopResultsTimer = setTimeout(timeoutStopRecordingTimer, 3000); + // we expect all clients to respond within 1 seconds to mimic the reliable UDP layer + stoppingSessionState.aggegratingStopResultsTimer = setTimeout(timeoutStopRecordingTimer, 1000); stoppingSessionState.recordingId = recordingId; stoppingSessionState.groupedClientTracks = copyClientIds(clients, app.clientId); diff --git a/web/app/assets/javascripts/findBand.js b/web/app/assets/javascripts/findBand.js index 144bf4541..644df8a9d 100644 --- a/web/app/assets/javascripts/findBand.js +++ b/web/app/assets/javascripts/findBand.js @@ -97,7 +97,7 @@ for (var kk=0, klen=iter_pinstruments.length; kk'; } diff --git a/web/app/assets/javascripts/findMusician.js b/web/app/assets/javascripts/findMusician.js index e10458b3b..38bf79976 100644 --- a/web/app/assets/javascripts/findMusician.js +++ b/web/app/assets/javascripts/findMusician.js @@ -41,14 +41,10 @@ queryString += "instrument=" + instrument + '&'; } - // distance filter - var query_param = $('#musician_query_distance').val(); - if (query_param !== null && query_param.length > 0) { - var matches = query_param.match(/(\d+)/); - if (0 < matches.length) { - var distance = matches[0]; - queryString += "distance=" + distance + '&'; - } + // score filter + var query_param = $('#musician_query_score').val(); + if (query_param !== null) { + queryString += "score_limit=" + query_param + '&'; } loadMusicians(queryString); } @@ -100,7 +96,7 @@ instr_logos = ''; for (var jj=0, ilen=musician['instruments'].length; jj'; } @@ -228,7 +224,7 @@ } function events() { - $('#musician_query_distance').change(refreshDisplay); + $('#musician_query_score').change(refreshDisplay); $('#musician_instrument').change(refreshDisplay); $('#musician_order_by').change(refreshDisplay); diff --git a/web/app/assets/javascripts/ga.js b/web/app/assets/javascripts/ga.js index a2d30f72b..bcbd8835c 100644 --- a/web/app/assets/javascripts/ga.js +++ b/web/app/assets/javascripts/ga.js @@ -79,13 +79,18 @@ band : 'Band', fan : 'Fan', recording : 'Recording', - session : 'Session' + session : 'Session', + facebook: 'facebook', + twitter: 'twitter', + google: 'google', }; var categories = { register : "Register", download : "DownloadClient", audioTest : "AudioTest", + trackConfig : "AudioTrackConfig", + networkTest : "NetworkTest", sessionCount : "SessionCount", sessionMusicians : "SessionMusicians", sessionQuality : "SessionQuality", @@ -271,13 +276,30 @@ context.ga('send', 'event', categories.band, bandAction); } - function trackJKSocial(category, target) { + function trackJKSocial(category, target, data) { assertOneOf(category, categories); assertOneOf(target, jkSocialTargets); - context.ga('send', 'event', category, target); + context.ga('send', 'event', category, target, data); } + function trackNetworkTest(platform, numUsers) { + var normalizedPlatform = translatePlatformForGA(platform); + + context.ga('send', 'event', categories.networkTest, 'Passed', normalizedPlatform, numUsers); + } + + function trackAudioTestCompletion(platform) { + var normalizedPlatform = translatePlatformForGA(platform); + + context.ga('send', 'event', categories.audioTest, 'Passed', normalizedPlatform); + } + + function trackConfigureTracksCompletion(platform) { + var normalizedPlatform = translatePlatformForGA(platform); + + context.ga('send', 'event', categories.trackConfig, 'Passed', normalizedPlatform); + } var GA = {}; GA.Categories = categories; @@ -291,6 +313,9 @@ GA.trackRegister = trackRegister; GA.trackDownload = trackDownload; GA.trackFTUECompletion = trackFTUECompletion; + GA.trackNetworkTest = trackNetworkTest; + GA.trackAudioTestCompletion = trackAudioTestCompletion; + GA.trackConfigureTracksCompletion = trackConfigureTracksCompletion; GA.trackSessionCount = trackSessionCount; GA.trackSessionMusicians = trackSessionMusicians; GA.trackSessionQuality = trackSessionQuality; diff --git a/web/app/assets/javascripts/gear_wizard.js b/web/app/assets/javascripts/gear_wizard.js deleted file mode 100644 index 9012c91be..000000000 --- a/web/app/assets/javascripts/gear_wizard.js +++ /dev/null @@ -1,909 +0,0 @@ -(function (context, $) { - - "use strict"; - - - context.JK = context.JK || {}; - context.JK.GearWizard = function (app) { - - var ASSIGNMENT = context.JK.ASSIGNMENT; - var VOICE_CHAT = context.JK.VOICE_CHAT; - - var $dialog = null; - var $wizardSteps = null; - var $currentWizardStep = null; - var step = 0; - var $templateSteps = null; - var $templateButtons = null; - var $templateAudioPort = null; - var $ftueButtons = null; - var self = null; - var operatingSystem = null; - - // populated by loadDevices - var deviceInformation = null; - var musicPorts = null; - - - var validLatencyScore = false; - var validIOScore = false; - - // SELECT TRACKS STATE - - var TOTAL_STEPS = 7; - var STEP_INTRO = 0; - var STEP_SELECT_DEVICE = 1; - var STEP_SELECT_TRACKS = 2; - var STEP_SELECT_CHAT = 3; - var STEP_DIRECT_MONITOR = 4; - var STEP_ROUTER_NETWORK = 5; - var STEP_SUCCESS = 6; - - var PROFILE_DEV_SEP_TOKEN = '^'; - - var iCheckIgnore = false; - - var audioDeviceBehavior = { - MacOSX_builtin: { - display: 'MacOSX Built-In', - videoURL: undefined - }, - MacOSX_interface: { - display: 'MacOSX external interface', - videoURL: undefined - }, - Win32_wdm: { - display: 'Windows WDM', - videoURL: undefined - }, - Win32_asio: { - display: 'Windows ASIO', - videoURL: undefined - }, - Win32_asio4all: { - display: 'Windows ASIO4ALL', - videoURL: undefined - }, - Linux: { - display: 'Linux', - videoURL: undefined - } - } - - function beforeShowIntro() { - var $watchVideo = $currentWizardStep.find('.watch-video'); - var videoUrl = 'https://www.youtube.com/watch?v=VexH4834o9I'; - if (operatingSystem == "Win32") { - $watchVideo.attr('href', 'https://www.youtube.com/watch?v=VexH4834o9I'); - } - $watchVideo.attr('href', videoUrl); - } - - function beforeSelectDevice() { - - var $watchVideoInput = $currentWizardStep.find('.watch-video.audio-input'); - var $watchVideoOutput = $currentWizardStep.find('.watch-video.audio-output'); - var $audioInput = $currentWizardStep.find('.select-audio-input-device'); - var $audioOutput = $currentWizardStep.find('.select-audio-output-device'); - var $bufferIn = $currentWizardStep.find('.select-buffer-in'); - var $bufferOut = $currentWizardStep.find('.select-buffer-out'); - var $frameSize = $currentWizardStep.find('.select-frame-size'); - var $inputChannels = $currentWizardStep.find('.input-ports'); - var $outputChannels = $currentWizardStep.find('.output-ports'); - var $scoreReport = $currentWizardStep.find('.results'); - var $latencyScoreSection = $scoreReport.find('.latency-score-section'); - var $latencyScore = $scoreReport.find('.latency-score'); - var $ioScoreSection = $scoreReport.find('.io-score-section'); - var $ioRateScore = $scoreReport.find('.io-rate-score'); - var $ioVarScore = $scoreReport.find('.io-var-score'); - var $ioCountdown = $scoreReport.find('.io-countdown'); - var $ioCountdownSecs = $scoreReport.find('.io-countdown .secs'); - var $nextButton = $ftueButtons.find('.btn-next'); - var $asioControlPanelBtn = $currentWizardStep.find('.asio-settings-btn'); - var $resyncBtn = $currentWizardStep.find('resync-btn') - - // should return one of: - // * MacOSX_builtin - // * MACOSX_interface - // * Win32_wdm - // * Win32_asio - // * Win32_asio4all - // * Linux - function determineDeviceType(deviceId, displayName) { - if (operatingSystem == "MacOSX") { - if (displayName.toLowerCase().trim() == "built-in") { - return "MacOSX_builtin"; - } - else { - return "MacOSX_interface"; - } - } - else if (operatingSystem == "Win32") { - if (context.jamClient.FTUEIsMusicDeviceWDM(deviceId)) { - return "Win32_wdm"; - } - else if (displayName.toLowerCase().indexOf("asio4all") > -1) { - return "Win32_asio4all" - } - else { - return "Win32_asio"; - } - } - else { - return "Linux"; - } - } - - function loadDevices() { - - var oldDevices = context.jamClient.FTUEGetDevices(false); - var devices = context.jamClient.FTUEGetAudioDevices(); - console.log("oldDevices: " + JSON.stringify(oldDevices)); - console.log("devices: " + JSON.stringify(devices)); - - var loadedDevices = {}; - - // augment these devices by determining their type - context._.each(devices.devices, function (device) { - - if(device.name == "JamKazam Virtual Monitor") { - return; - } - - var deviceInfo = {}; - - deviceInfo.id = device.guid; - deviceInfo.type = determineDeviceType(device.guid, device.display_name); - console.log("deviceInfo.type: " + deviceInfo.type) - deviceInfo.displayType = audioDeviceBehavior[deviceInfo.type].display; - deviceInfo.displayName = device.display_name; - - loadedDevices[device.guid] = deviceInfo; - - logger.debug("loaded device: ", deviceInfo); - }) - - deviceInformation = loadedDevices; - - logger.debug(context.JK.dlen(deviceInformation) + " devices loaded.", deviceInformation); - } - - // returns a deviceInfo hash for the device matching the deviceId, or undefined. - function findDevice(deviceId) { - return deviceInformation[deviceId]; - } - - function selectedAudioInput() { - return $audioInput.val(); - } - - function selectedAudioOutput() { - return $audioOutput.val(); - } - - function selectedFramesize() { - return parseFloat($frameSize.val()); - } - - function selectedBufferIn() { - return parseFloat($frameSize.val()); - } - - function selectedBufferOut() { - return parseFloat($frameSize.val()); - } - - function initializeNextButtonState() { - $nextButton.removeClass('button-orange button-grey'); - - if (validLatencyScore) $nextButton.addClass('button-orange'); - else $nextButton.addClass('button-grey'); - } - - function initializeAudioInput() { - var optionsHtml = ''; - optionsHtml = ''; - context._.each(deviceInformation, function (deviceInfo, deviceId) { - - console.log(arguments) - optionsHtml += ''; - }); - $audioInput.html(optionsHtml); - context.JK.dropdown($audioInput); - - initializeAudioInputChanged(); - } - - function initializeAudioOutput() { - var optionsHtml = ''; - optionsHtml = ''; - context._.each(deviceInformation, function (deviceInfo, deviceId) { - optionsHtml += ''; - }); - $audioOutput.html(optionsHtml); - context.JK.dropdown($audioOutput); - - initializeAudioOutputChanged(); - } - - function initializeFramesize() { - context.JK.dropdown($frameSize); - } - - function initializeBuffers() { - context.JK.dropdown($bufferIn); - context.JK.dropdown($bufferOut); - } - - - // reloads the backend's channel state for the currently selected audio devices, - // and update's the UI accordingly - function initializeChannels() { - musicPorts = jamClient.FTUEGetChannels(); - console.log("musicPorts: %o", JSON.stringify(musicPorts)); - - initializeInputPorts(musicPorts); - initializeOutputPorts(musicPorts); - } - - // during this phase of the FTUE, we have to assign selected input channels - // to tracks. The user, however, does not have a way to indicate which channel - // goes to which track (that's not until the next step of the wizard). - // so, we just auto-generate a valid assignment - function newInputAssignment() { - var assigned = 0; - context._.each(musicPorts.inputs, function(inputChannel) { - if(isChannelAssigned(inputChannel)) { - assigned += 1; - } - }); - - var newAssignment = Math.floor(assigned / 2) + 1; - return newAssignment; - } - - function inputChannelChanged() { - if(iCheckIgnore) return; - - var $checkbox = $(this); - var channelId = $checkbox.attr('data-id'); - var isChecked = $checkbox.is(':checked'); - - if(isChecked) { - var newAssignment = newInputAssignment(); - logger.debug("assigning input channel %o to track: %o", channelId, newAssignment); - context.jamClient.TrackSetAssignment(channelId, true, newAssignment); - } - else { - logger.debug("unassigning input channel %o", channelId); - context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.UNASSIGNED); - // unassigning creates a hole in our auto-assigned tracks. reassign them all to keep it consistent - var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked'); - var assigned = 0; - context._.each($assignedInputs, function(assignedInput) { - var $assignedInput = $(assignedInput); - var assignedChannelId = $assignedInput.attr('data-id'); - var newAssignment = Math.floor(assigned / 2) + 1; - logger.debug("re-assigning input channel %o to track: %o", assignedChannelId, newAssignment); - context.jamClient.TrackSetAssignment(assignedChannelId, true, newAssignment); - assigned += 1; - }); - } - - initializeChannels(); - } - - // should be called in a ifChanged callback if you want to cancel. - // you have to use this instead of 'return false' like a typical input 'change' event. - function cancelICheckChange($checkbox) { - iCheckIgnore = true; - var checked = $checkbox.is(':checked'); - setTimeout(function() { - if(checked) $checkbox.iCheck('uncheck').removeAttr('checked'); - else $checkbox.iCheck('check').attr('checked', 'checked'); - iCheckIgnore = false; - }, 1); - } - - function outputChannelChanged() { - if(iCheckIgnore) return; - var $checkbox = $(this); - var channelId = $checkbox.attr('data-id'); - var isChecked = $checkbox.is(':checked'); - - // don't allow more than 2 output channels selected at once - if($outputChannels.find('input[type="checkbox"]:checked').length > 2) { - context.JK.Banner.showAlert('You can only have a maximum of 2 output ports selected.'); - // can't allow uncheck of last output - cancelICheckChange($checkbox); - return; - } - - if(isChecked) { - logger.debug("assigning output channel %o", channelId); - context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.OUTPUT); - } - else { - logger.debug("unassigning output channel %o", channelId); - context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.UNASSIGNED); - } - - initializeChannels(); - } - - // checks if it's an assigned OUTPUT or ASSIGNED CHAT - function isChannelAssigned(channel) { - return channel.assignment == ASSIGNMENT.CHAT || channel.assignment == ASSIGNMENT.OUTPUT || channel.assignment > 0; - } - - function initializeInputPorts(musicPorts) { - $inputChannels.empty(); - var inputPorts = musicPorts.inputs; - context._.each(inputPorts, function(inputChannel) { - var $inputChannel = $(context._.template($templateAudioPort.html(), inputChannel, { variable: 'data' })); - var $checkbox = $inputChannel.find('input'); - if(isChannelAssigned(inputChannel)) { - $checkbox.attr('checked', 'checked'); - } - context.JK.checkbox($checkbox); - $checkbox.on('ifChanged', inputChannelChanged); - $inputChannels.append($inputChannel); - }); - } - - function initializeOutputPorts(musicPorts) { - $outputChannels.empty(); - var outputChannels = musicPorts.outputs; - context._.each(outputChannels, function(outputChannel) { - var $outputPort = $(context._.template($templateAudioPort.html(), outputChannel, { variable: 'data' })); - var $checkbox = $outputPort.find('input'); - if(isChannelAssigned(outputChannel)) { - $checkbox.attr('checked', 'checked'); - } - context.JK.checkbox($checkbox); - $checkbox.on('ifChanged', outputChannelChanged); - $outputChannels.append($outputPort); - }); - } - - function initializeFormElements() { - if (!deviceInformation) throw "devices are not initialized"; - - initializeAudioInput(); - initializeAudioOutput(); - initializeFramesize(); - initializeBuffers(); - } - - function resetFrameBuffers() { - $frameSize.val('2.5'); - $bufferIn.val('0'); - $bufferOut.val('0'); - } - - function clearInputPorts() { - $inputChannels.empty(); - } - - function clearOutputPorts() { - $outputChannels.empty(); - } - - function resetScoreReport() { - $ioRateScore.empty(); - $ioVarScore.empty(); - $latencyScore.empty(); - } - - function renderLatencyScore(latencyValue, latencyClass) { - if(latencyValue) { - $latencyScore.text(latencyValue + ' ms'); - } - else { - $latencyScore.text(''); - } - $latencyScoreSection.removeClass('good acceptable bad unknown starting').addClass(latencyClass); - } - - // std deviation is the worst value between in/out - // media is the worst value between in/out - // io is the value returned by the backend, which has more info - // ioClass is the pre-computed rollup class describing the result in simple terms of 'good', 'acceptable', bad' - function renderIOScore(std, median, ioData, ioClass) { - $ioRateScore.text(median ? median : ''); - $ioVarScore.text(std ? std : ''); - $ioScoreSection.removeClass('good acceptable bad unknown starting skip').addClass(ioClass); - // TODO: show help bubble of all data in IO data - } - - function updateScoreReport(latencyResult) { - var latencyClass = "neutral"; - var latencyValue = 'N/A'; - var validLatency = false; - if (latencyResult && latencyResult.latencyknown) { - var latencyValue = latencyResult.latency; - latencyValue = Math.round(latencyValue * 100) / 100; - if (latencyValue <= 10) { - latencyClass = "good"; - validLatency = true; - } else if (latencyValue <= 20) { - latencyClass = "acceptable"; - validLatency = true; - } else { - latencyClass = "bad"; - } - } - else { - latencyClass = 'unknown'; - } - - validLatencyScore = validLatency; - - renderLatencyScore(latencyValue, latencyClass); - } - - function audioInputDeviceUnselected() { - validLatencyScore = false; - initializeNextButtonState(); - resetFrameBuffers(); - clearInputPorts(); - } - - function renderScoringStarted() { - validLatencyScore = false; - initializeNextButtonState(); - resetScoreReport(); - freezeAudioInteraction(); - renderLatencyScore(null, 'starting'); - } - - function renderScoringStopped() { - initializeNextButtonState(); - unfreezeAudioInteraction(); - } - - - function freezeAudioInteraction() { - $audioInput.attr("disabled", "disabled").easyDropDown('disable'); - $audioOutput.attr("disabled", "disabled").easyDropDown('disable'); - $frameSize.attr("disabled", "disabled").easyDropDown('disable'); - $bufferIn.attr("disabled", "disabled").easyDropDown('disable'); - $bufferOut.attr("disabled", "disabled").easyDropDown('disable'); - $asioControlPanelBtn.on("click", false); - $resyncBtn.on('click', false); - iCheckIgnore = true; - $inputChannels.find('input[type="checkbox"]').iCheck('disable'); - $outputChannels.find('input[type="checkbox"]').iCheck('disable'); - } - - function unfreezeAudioInteraction() { - $audioInput.removeAttr("disabled").easyDropDown('enable'); - $audioOutput.removeAttr("disabled").easyDropDown('enable'); - $frameSize.removeAttr("disabled").easyDropDown('enable'); - $bufferIn.removeAttr("disabled").easyDropDown('enable'); - $bufferOut.removeAttr("disabled").easyDropDown('enable'); - $asioControlPanelBtn.off("click", false); - $resyncBtn.off('click', false); - $inputChannels.find('input[type="checkbox"]').iCheck('enable'); - $outputChannels.find('input[type="checkbox"]').iCheck('enable'); - iCheckIgnore = false; - } - - // Given a latency structure, update the view. - function newFtueUpdateLatencyView(latency) { - var $report = $('.ftue-new .latency .report'); - var $instructions = $('.ftue-new .latency .instructions'); - var latencyClass = "neutral"; - var latencyValue = "N/A"; - var $saveButton = $('#btn-ftue-2-save'); - if (latency && latency.latencyknown) { - latencyValue = latency.latency; - // Round latency to two decimal places. - latencyValue = Math.round(latencyValue * 100) / 100; - if (latency.latency <= 10) { - latencyClass = "good"; - setSaveButtonState($saveButton, true); - } else if (latency.latency <= 20) { - latencyClass = "acceptable"; - setSaveButtonState($saveButton, true); - } else { - latencyClass = "bad"; - setSaveButtonState($saveButton, false); - } - } else { - latencyClass = "unknown"; - setSaveButtonState($saveButton, false); - } - - $('.ms-label', $report).html(latencyValue); - $('p', $report).html('milliseconds'); - - $report.removeClass('good acceptable bad unknown'); - $report.addClass(latencyClass); - - var instructionClasses = ['neutral', 'good', 'acceptable', 'unknown', 'bad', 'start', 'loading']; - $.each(instructionClasses, function (idx, val) { - $('p.' + val, $instructions).hide(); - }); - if (latency === 'loading') { - $('p.loading', $instructions).show(); - } else { - $('p.' + latencyClass, $instructions).show(); - renderStopNewFtueLatencyTesting(); - } - } - - function initializeWatchVideo() { - $watchVideoInput.unbind('click').click(function () { - - var audioDevice = findDevice(selectedAudioInput()); - if (!audioDevice) { - context.JK.Banner.showAlert('You must first choose an Audio Input Device so that we can determine which video to show you.'); - } - else { - var videoURL = audioDeviceBehavior[audioDevice.type].videoURL; - - if (videoURL) { - $(this).attr('href', videoURL); - return true; - } - else { - context.JK.Banner.showAlert('No help video for this type of device (' + audioDevice.displayType + ')'); - } - } - - return false; - }); - - $watchVideoOutput.unbind('click').click(function () { - - var audioDevice = findDevice(selectedAudioOutput()); - if (!audioDevice) { - throw "this button should be hidden"; - } - else { - var videoURL = audioDeviceBehavior[audioDevice.type].videoURL; - - if (videoURL) { - $(this).attr('href', videoURL); - return true; - } - else { - context.JK.Banner.showAlert('No help video for this type of device (' + audioDevice.displayType + ')'); - } - } - - return false; - }); - } - - function renderIOScoringStarted(secondsLeft) { - $ioCountdownSecs.text(secondsLeft); - $ioCountdown.show(); - } - - function renderIOScoringStopped() { - $ioCountdown.hide(); - } - - function renderIOCountdown(secondsLeft) { - $ioCountdownSecs.text(secondsLeft); - } - - function attemptScore() { - var audioInputDeviceId = selectedAudioInput(); - var audioOutputDeviceId = selectedAudioOutput(); - if (!audioInputDeviceId) { - audioInputDeviceUnselected(); - return false; - } - - var audioInputDevice = findDevice(audioInputDeviceId); - if (!audioInputDevice) { - context.JK.alertSupportedNeeded('Unable to find information for input device: ' + audioInputDeviceId); - return false; - } - - if(!audioOutputDeviceId) { - audioOutputDeviceId = audioInputDeviceId; - } - var audioOutputDevice = findDevice(audioOutputDeviceId); - if (!audioInputDevice) { - context.JK.alertSupportedNeeded('Unable to find information for output device: ' + audioOutputDeviceId); - return false; - } - - jamClient.FTUESetInputMusicDevice(audioInputDeviceId); - jamClient.FTUESetOutputMusicDevice(audioOutputDeviceId); - - initializeChannels(); - - jamClient.FTUESetInputLatency(selectedBufferIn()); - jamClient.FTUESetOutputLatency(selectedBufferOut()); - jamClient.FTUESetFrameSize(selectedFramesize()); - - renderScoringStarted(); - logger.debug("Calling FTUESave(false)"); - jamClient.FTUESave(false); - - var latency = jamClient.FTUEGetExpectedLatency(); - console.log("FTUEGetExpectedLatency: %o", latency); - - updateScoreReport(latency); - - // if there was a valid latency score, go on to the next step - if(validLatencyScore) { - renderIOScore(null, null, null, 'starting'); - var testTimeSeconds = 10; // allow 10 seconds for IO to establish itself - context.jamClient.FTUEStartIoPerfTest(); - renderIOScoringStarted(testTimeSeconds); - renderIOCountdown(testTimeSeconds); - var interval = setInterval(function() { - testTimeSeconds -= 1; - renderIOCountdown(testTimeSeconds); - if(testTimeSeconds == 0) { - clearInterval(interval); - renderIOScoringStopped(); - var io = context.jamClient.FTUEGetIoPerfData(); - - console.log("io: ", io); - - // take the higher variance, which is apparently actually std dev - var std = io.in_var > io.out_var ? io.in_var : io.out_var; - std = Math.round(std * 100) / 100; - // take the furthest-off-from-target io rate - var median = Math.abs(io.in_median - io.in_target ) > Math.abs(io.out_median - io.out_target ) ? [io.in_median, io.in_target] : [io.out_median, io.out_target]; - var medianTarget = median[1]; - median = Math.round(median[0]); - - var stdIOClass = 'bad'; - if(std <= 0.50) { - stdIOClass = 'good'; - } - else if(std <= 1.00) { - stdIOClass = 'acceptable'; - } - - var medianIOClass = 'bad'; - if(Math.abs(median - medianTarget) <= 1) { - medianIOClass = 'good'; - } - else if(Math.abs(median - medianTarget) <= 2) { - medianIOClass = 'acceptable'; - } - - // now base the overall IO score based on both values. - renderIOScore(std, median, io, ioClass); - - // lie for now until IO questions finalize - validIOScore = true; - - renderScoringStopped(); - } - }, 1000); - } - else { - renderIOScore(null, null, null, 'skip'); - renderScoringStopped(); - } - - } - - function initializeAudioInputChanged() { - $audioInput.unbind('change').change(attemptScore); - } - - function initializeAudioOutputChanged() { - - } - - function initializeStep() { - loadDevices(); - initializeFormElements(); - initializeNextButtonState(); - initializeWatchVideo(); - } - - initializeStep(); - } - - function beforeSelectTracks() { - - } - - function beforeSelectChat() { - - } - - function beforeDirectMonitor() { - - } - - function beforeTestNetwork() { - - } - - function beforeSuccess() { - - } - - var STEPS = { - 0: { - beforeShow: beforeShowIntro - }, - 1: { - beforeShow: beforeSelectDevice - }, - 2: { - beforeShow: beforeSelectTracks - }, - 3: { - beforeShow: beforeSelectChat - }, - 4: { - beforeShow: beforeDirectMonitor - }, - 5: { - beforeShow: beforeTestNetwork - }, - 6: { - beforeShow: beforeSuccess - } - } - - function beforeShowStep($step) { - var stepInfo = STEPS[step]; - - if (!stepInfo) { - throw "unknown step: " + step; - } - - stepInfo.beforeShow.call(self); - } - - function moveToStep() { - var $nextWizardStep = $wizardSteps.filter($('[layout-wizard-step=' + step + ']')); - - $wizardSteps.hide(); - - $currentWizardStep = $nextWizardStep; - - var $ftueSteps = $(context._.template($templateSteps.html(), {}, { variable: 'data' })); - var $activeStep = $ftueSteps.find('.ftue-stepnumber[data-step-number="' + step + '"]'); - $activeStep.addClass('.active'); - $activeStep.next().show(); // show the .ftue-step-title - $currentWizardStep.find('.ftuesteps').replaceWith($ftueSteps); - beforeShowStep($currentWizardStep); - $currentWizardStep.show(); - - // update buttons - var $ftueButtonsContent = $(context._.template($templateButtons.html(), {}, {variable: 'data'})); - - - var $btnBack = $ftueButtonsContent.find('.btn-back'); - var $btnNext = $ftueButtonsContent.find('.btn-next'); - var $btnClose = $ftueButtonsContent.find('.btn-close'); - var $btnCancel = $ftueButtonsContent.find('.btn-cancel'); - - // hide back button if 1st step or last step - if (step == 0 && step == TOTAL_STEPS - 1) { - $btnBack.hide(); - } - // hide next button if not on last step - if (step == TOTAL_STEPS - 1) { - $btnNext.hide(); - } - // hide close if on last step - if (step != TOTAL_STEPS - 1) { - $btnClose.hide(); - } - // hide cancel if not on last step - if (step == TOTAL_STEPS - 1) { - $btnCancel.hide(); - } - - $btnNext.on('click', next); - $btnBack.on('click', back); - $btnClose.on('click', closeDialog); - $btnCancel.on('click', closeDialog); - - $ftueButtons.empty(); - $ftueButtons.append($ftueButtonsContent); - } - - function reset() { - $currentWizardStep = null; - } - - // checks if we already have a profile called 'FTUE...'; if not, create one. if so, re-use it. - function findOrCreateFTUEProfile() { - var profileName = context.jamClient.FTUEGetMusicProfileName(); - - logger.debug("current profile name: " + profileName); - - if(profileName && profileName.indexOf('FTUE') == 0) { - - } - else { - var newProfileName = 'FTUEAttempt-' + new Date().getTime().toString(); - logger.debug("setting FTUE-prefixed profile name to: " + newProfileName); - context.jamClient.FTUESetMusicProfileName(newProfileName); - } - - var profileName = context.jamClient.FTUEGetMusicProfileName(); - - logger.debug("name on exit: " + profileName); - - } - - function beforeShow(args) { - context.jamClient.FTUECancel(); - findOrCreateFTUEProfile(); - - step = args.d1; - if (!step) step = 0; - step = parseInt(step); - moveToStep(); - } - - function afterShow() { - - } - - function afterHide() { - context.jamClient.FTUECancel(); - } - - function back() { - if ($(this).is('.button-grey')) return; - step = step - 1; - moveToStep(); - return false; - } - - function next() { - if ($(this).is('.button-grey')) return; - - step = step + 1; - - moveToStep(); - return false; - } - - function closeDialog() { - app.layout.closeDialog('gear-wizard'); - return false; - } - - function events() { - } - - function route() { - - } - - function initialize() { - - var dialogBindings = { beforeShow: beforeShow, afterShow: afterShow, afterHide: afterHide }; - - app.bindDialog('gear-wizard', dialogBindings); - - $dialog = $('#gear-wizard-dialog'); - $wizardSteps = $dialog.find('.wizard-step'); - $templateSteps = $('#template-ftuesteps'); - $templateButtons = $('#template-ftue-buttons'); - $templateAudioPort = $('#template-audio-port'); - $ftueButtons = $dialog.find('.ftue-buttons'); - - operatingSystem = context.jamClient.GetOSAsString(); - - events(); - } - - this.initialize = initialize; - - self = this; - return this; - }; - -})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index 7c3a37ec3..9f32fb61d 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -27,6 +27,8 @@ CHAT: "1" }; + context.JK.MAX_TRACKS = 6; + // TODO: store these client_id values in instruments table, or store // server_id as the client_id to prevent maintenance nightmares. As it's // set up now, we will have to deploy each time we add new instruments. @@ -102,4 +104,43 @@ context.JK.entityToPrintable = { music_session: "music session" } + + context.JK.AUDIO_DEVICE_BEHAVIOR = { + MacOSX_builtin: { + display: 'MacOSX Built-In', + videoURL: undefined, + showKnobs: false, + showASIO: false + }, + MacOSX_interface: { + display: 'MacOSX external interface', + videoURL: undefined, + showKnobs: false, + showASIO: false + }, + Win32_wdm: { + display: 'Windows WDM', + videoURL: undefined, + showKnobs: true, + showASIO: false + }, + Win32_asio: { + display: 'Windows ASIO', + videoURL: undefined, + showKnobs: false, + showASIO: true + }, + Win32_asio4all: { + display: 'Windows ASIO4ALL', + videoURL: undefined, + showKnobs: false, + showASIO: true + }, + Linux: { + display: 'Linux', + videoURL: undefined, + showKnobs: true, + showASIO: false + } + } })(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 497667cb9..9eb526b69 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -142,11 +142,34 @@ }); } - function submitRsvpRequest(sessionId) { + function getRsvpRequests(sessionId) { + return $.ajax({ + url: '/api/rsvp_requests?session_id=' + sessionId, + type: "GET", + dataType : 'json', + contentType: 'application/json' + }); + } + + function submitRsvpRequest(sessionId, slotIds) { return $.ajax({ url: '/api/rsvp_requests', type: "POST", - data : JSON.stringify({"session_id": sessionId}), + data : JSON.stringify({"session_id": sessionId, "rsvp_slots": slotIds}), + dataType : 'json', + contentType: 'application/json' + }); + } + + function cancelRsvpRequest(sessionId, rsvpRequestId, cancelAll) { + var cancel = "yes"; + if (cancelAll) { + cancel = "all"; + } + return $.ajax({ + url: '/api/rsvp_requests/' + rsvpRequestId, + type: "DELETE", + data : JSON.stringify({"session_id": sessionId, "cancelled": cancel}), dataType : 'json', contentType: 'application/json' }); @@ -1046,11 +1069,28 @@ }; function createDiagnostic(options) { + var data = null; + try { + data = JSON.stringify(options) + } + catch(e) { + data = JSON.stringify({data_error: "unable to JSON.stringify debug data:" + e.toString()}) + } return $.ajax({ type: "POST", url: '/api/diagnostics', dataType: "json", contentType: 'application/json', + data: data, + }); + } + + function getLatencyTester(options) { + return $.ajax({ + type: "GET", + url: '/api/latency_testers', + dataType: "json", + contentType: 'application/json', data: JSON.stringify(options) }); } @@ -1094,7 +1134,9 @@ this.addSessionComment = addSessionComment; this.addSessionInfoComment = addSessionInfoComment; this.addSessionLike = addSessionLike; + this.getRsvpRequests = getRsvpRequests; this.submitRsvpRequest = submitRsvpRequest; + this.cancelRsvpRequest = cancelRsvpRequest; this.getOpenSessionSlots = getOpenSessionSlots; this.addRecordingComment = addRecordingComment; this.addRecordingLike = addRecordingLike; @@ -1147,6 +1189,7 @@ this.createChatMessage = createChatMessage; this.getChatMessages = getChatMessages; this.createDiagnostic = createDiagnostic; + this.getLatencyTester = getLatencyTester; return this; }; diff --git a/web/app/assets/javascripts/jquery.instrumentSelector.js b/web/app/assets/javascripts/jquery.instrumentSelector.js new file mode 100644 index 000000000..eca325fc1 --- /dev/null +++ b/web/app/assets/javascripts/jquery.instrumentSelector.js @@ -0,0 +1,86 @@ +(function(context, $) { + + "use strict"; + + context.JK = context.JK || {}; + + + // creates an iconic/graphical instrument selector. useful when there is minimal real-estate + + $.fn.instrumentSelector = function(options) { + + return this.each(function(index) { + + function select(instrument_id) { + if(instrument_id == null) { + $currentInstrument.text('?'); + $currentInstrument.addClass('none'); + $select.data('instrument_id', null); + } + else { + $currentInstrument.empty(); + $currentInstrument.removeClass('none'); + $currentInstrument.append(''); + $select.data('instrument_id', instrument_id); + } + } + + function close() { + $currentInstrument.btOff(); + $currentInstrument.focus(); + } + + function onInstrumentSelected() { + var $li = $(this); + var instrument_id = $li.attr('data-instrument-id'); + + select(instrument_id); + close(); + $select.triggerHandler('instrument_selected', {instrument_id: instrument_id}); + return false; + }; + + var $select = $(context._.template($('#template-icon-instrument-select').html(), {instruments:context.JK.getInstrumentIconMap24()}, { variable: 'data' })); + var $ul = $select.find('ul'); + var $currentInstrument = $select.find('.current-instrument'); + + context.JK.hoverBubble($currentInstrument, $ul.html(), { + trigger:'click', + cssClass: 'icon-instrument-selector-popup', + spikeGirth:0, + spikeLength:0, + width:150, + closeWhenOthersOpen: true, + preShow: function() { + }, + postShow:function(container) { + $(container).find('li').click(onInstrumentSelected) + } + }); + + $currentInstrument.text('?'); + + $(this).append($select); + + this.instrumentSelectorClose = close; + this.instrumentSelectorSet = select; + }); + } + + $.fn.instrumentSelectorClose = function() { + return this.each(function(index){ + if (jQuery.isFunction(this.instrumentSelectorClose)) { + this.instrumentSelectorClose(); + } + }); + } + + $.fn.instrumentSelectorSet = function(instrumentId) { + return this.each(function(index){ + if (jQuery.isFunction(this.instrumentSelectorSet)) { + this.instrumentSelectorSet(instrumentId); + } + }); + } + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index b7cacd3a0..46cbdc041 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -677,10 +677,10 @@ } function startNewFtue() { - var step = 0; - setWizardStep(step); - wizardShowFunctions[step](); - showDialog('ftue'); + // var step = 0; + //setWizardStep(step); + //wizardShowFunctions[step](); + showDialog('gear-wizard'); } function setWizardStep(targetStepId) { diff --git a/web/app/assets/javascripts/profile.js b/web/app/assets/javascripts/profile.js index a8b15cdd0..b8e2a5810 100644 --- a/web/app/assets/javascripts/profile.js +++ b/web/app/assets/javascripts/profile.js @@ -614,7 +614,7 @@ var instrument = musician.instruments[j]; var inst = '/assets/content/icon_instrument_default24.png'; if (instrument.instrument_id in instrument_logo_map) { - inst = instrument_logo_map[instrument.instrument_id]; + inst = instrument_logo_map[instrument.instrument_id].asset; } instrumentLogoHtml += ' '; } diff --git a/web/app/assets/javascripts/rsvpCancelDialog.js b/web/app/assets/javascripts/rsvpCancelDialog.js index e69de29bb..a0a610cb5 100644 --- a/web/app/assets/javascripts/rsvpCancelDialog.js +++ b/web/app/assets/javascripts/rsvpCancelDialog.js @@ -0,0 +1,94 @@ +(function(context,$) { + + "use strict"; + context.JK = context.JK || {}; + context.JK.RsvpCancelDialog = function(app, sessionId, rsvpRequestId) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var $screen = null; + var dialogId = 'rsvp-cancel-dialog'; + var $btnCancel = $("#btnCancelRsvp"); + + function beforeShow(data) { + } + + function afterShow(data) { + + rest.getSessionHistory(sessionId) + .done(function(response) { + if (response) { + $('.session-name', $screen).html(response.name); + $('.scheduled-start', $screen).html(response.scheduled_start); + + if (response.recurring_mode !== null) { + $('.schedule-recurrence', $screen).html("Recurs " + response.recurring_mode + " on this day at this time"); + } + } + }) + .fail(function(xhr) { + + }); + } + + function afterHide() { + } + + function showDialog() { + app.layout.showDialog('rsvp-cancel-dialog'); + } + + function events() { + $btnCancel.unbind('click'); + $btnCancel.click(function(e) { + e.preventDefault(); + + var error = false; + var cancelOption = $('input[name="cancel"]:checked', $screen).val(); + rest.cancelRsvpRequest(sessionId, rsvpRequestId, cancelOption) + .done(function(response) { + var comment = $.trim($('#txtComment', $screen).val()); + if (comment.length > 0) { + rest.addSessionInfoComment(sessionId, comment) + .done(function(response) { + + }) + .fail(function(xhr) { + error = true; + $('.error', $screen).html("Unexpected error occurred while saving message (" + xhr.status + ")"); + $('.error', $screen).show(); + }); + } + + if (!error) { + app.layout.closeDialog(dialogId); + $btnCancel.trigger("rsvpCancelEvent"); + } + }) + .fail(function(xhr) { + $('.error', $screen).html("Unexpected error occurred while cancelling RSVP request (" + xhr.status + ")"); + $('.error', $screen).show(); + }); + }); + } + + function initialize() { + + var dialogBindings = { + 'beforeShow' : beforeShow, + 'afterShow' : afterShow, + 'afterHide': afterHide + }; + + app.bindDialog(dialogId, dialogBindings); + + $screen = $('[layout-id="' + dialogId + '"]'); + + events(); + } + + this.initialize = initialize; + this.showDialog = showDialog; + } + + return this; +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/rsvpSubmitDialog.js b/web/app/assets/javascripts/rsvpSubmitDialog.js index 1bb5d4a32..3138c8864 100644 --- a/web/app/assets/javascripts/rsvpSubmitDialog.js +++ b/web/app/assets/javascripts/rsvpSubmitDialog.js @@ -6,6 +6,8 @@ var logger = context.JK.logger; var rest = context.JK.Rest(); var $screen = null; + var dialogId = 'rsvp-submit-dialog'; + var $btnSubmit = $("#btnSubmitRsvp"); function beforeShow(data) { } @@ -28,7 +30,6 @@ }); - // if the session has slots, get the open ones rest.getOpenSessionSlots(sessionId, true) .done(function(response) { if (response && response.length > 0) { @@ -36,9 +37,7 @@ var instrument = val.instrument_id; var instrumentTitleCase = context.JK.toTitleCase(instrument); - // var instrumentTitleCase = instrument.charAt(0).toUpperCase() + instrument.substr(1).toLowerCase(); - // var instTitleCase = val.instrument_id.charAt(0).toUpperCase() + context.val.instrument_id.charAt(0) - $('.rsvp-instruments', $screen).append('' + instrumentTitleCase + "
"); + $('.rsvp-instruments', $screen).append('' + instrumentTitleCase + "
"); }); } else { @@ -55,14 +54,26 @@ } function showDialog() { - app.layout.showDialog('rsvp-submit-dialog'); + app.layout.showDialog(dialogId); } function events() { - $("#btnSubmit").click(function(e) { - rest.submitRsvpRequest(sessionId) + $btnSubmit.unbind('click'); + $btnSubmit.click(function(e) { + e.preventDefault(); + var slotIds = []; + $("input:checked", '.rsvp-instruments').each(function(index) { + slotIds.push($(this).val()); + }); + + if (slotIds.length === 0) { + $('.error', $screen).show(); + return; + } + + var error = false; + rest.submitRsvpRequest(sessionId, slotIds) .done(function(response) { - var comment = $.trim($('#txtComment', $screen).val()); if (comment.length > 0) { rest.addSessionInfoComment(sessionId, comment) @@ -70,14 +81,20 @@ }) .fail(function(xhr) { - + error = true; + $('.error', $screen).html("Unexpected error occurred while saving message (" + xhr.status + ")"); + $('.error', $screen).show(); }); } - app.layout.cDialog('rsvp-submit-dialog'); + if (!error) { + app.layout.closeDialog(dialogId); + $btnSubmit.trigger("rsvpSubmitEvent"); + } }) .fail(function(xhr) { - + $('.error', $screen).html("Unexpected error occurred while saving RSVP request (" + xhr.status + ")"); + $('.error', $screen).show(); }); }); } @@ -90,9 +107,9 @@ 'afterHide': afterHide }; - app.bindDialog('rsvp-submit-dialog', dialogBindings); + app.bindDialog(dialogId, dialogBindings); - $screen = $('[layout-id="rsvp-submit-dialog"]'); + $screen = $('[layout-id="' + dialogId + '"]'); events(); } diff --git a/web/app/assets/javascripts/searchResults.js b/web/app/assets/javascripts/searchResults.js index 1af9398e5..ddea50655 100644 --- a/web/app/assets/javascripts/searchResults.js +++ b/web/app/assets/javascripts/searchResults.js @@ -226,7 +226,7 @@ for (var i=0; i < instruments.length; i++) { var inst = '../assets/content/icon_instrument_default24.png'; if (instruments[i].instrument_id in instrument_logo_map) { - inst = instrument_logo_map[instruments[i].instrument_id]; + inst = instrument_logo_map[instruments[i].instrument_id].asset; instrumentLogoHtml += ' '; } } diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index d1ffbb7bb..b72fc9511 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -385,6 +385,7 @@ }) .on('stoppedRecording', function(e, data) { if(data.reason) { + logger.warn("Recording Discarded: ", data); var reason = data.reason; var detail = data.detail; @@ -1544,6 +1545,10 @@ 'beforeDisconnect' : beforeDisconnect, }; app.bindScreen('session', screenBindings); + + // make sure no previous plays are still going on by accident + context.jamClient.SessionStopPlay(); + context.jamClient.SessionRemoveAllPlayTracks(); }; diff --git a/web/app/assets/javascripts/sessionList.js b/web/app/assets/javascripts/sessionList.js index b86bc1919..5716cce61 100644 --- a/web/app/assets/javascripts/sessionList.js +++ b/web/app/assets/javascripts/sessionList.js @@ -93,7 +93,7 @@ logger.debug("Find:Finding instruments. Participant tracks:", participant.tracks); var inst = '../assets/content/icon_instrument_default24.png'; if (track.instrument_id in instrument_logo_map) { - inst = instrument_logo_map[track.instrument_id]; + inst = instrument_logo_map[track.instrument_id].asset; } instrumentLogoHtml += ' '; } diff --git a/web/app/assets/javascripts/textMessageDialog.js b/web/app/assets/javascripts/textMessageDialog.js index 873bbc82d..f2e67a0c0 100644 --- a/web/app/assets/javascripts/textMessageDialog.js +++ b/web/app/assets/javascripts/textMessageDialog.js @@ -201,11 +201,13 @@ } function renderNotConnected() { + console.log("RENDER NOT CONNECTED!!!!!!!!!") $interactionBlocker.addClass('active'); $disconnectedMsg.addClass('active'); } function renderConnected() { + console.log("RENDER CONNECTED!!!!!!!!!") $interactionBlocker.removeClass('active'); $disconnectedMsg.removeClass('active'); } diff --git a/web/app/assets/javascripts/trackHelpers.js b/web/app/assets/javascripts/trackHelpers.js index 185645b81..384fd5dc3 100644 --- a/web/app/assets/javascripts/trackHelpers.js +++ b/web/app/assets/javascripts/trackHelpers.js @@ -46,7 +46,13 @@ for (i=0; i < localMusicTracks.length; i++) { var track = {}; track.client_track_id = localMusicTracks[i].id; - track.instrument_id = context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id].server_id; + + if(localMusicTracks[i].instrument_id === 0) { + track.instrument_id = context.JK.server_to_client_instrument_map["Other"].server_id; + } + else { + track.instrument_id = context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id].server_id; + } if (localMusicTracks[i].stereo) { track.sound = "stereo"; } diff --git a/web/app/assets/javascripts/ui_helper.js b/web/app/assets/javascripts/ui_helper.js index a4b1240d1..9bff51a7d 100644 --- a/web/app/assets/javascripts/ui_helper.js +++ b/web/app/assets/javascripts/ui_helper.js @@ -41,8 +41,8 @@ rsvpDialog.showDialog(); } - function launchRsvpCancelDialog(sessionId) { - var rsvpDialog = new JK.RsvpCancelDialog(JK.app, sessionId); + function launchRsvpCancelDialog(sessionId, rsvpRequestId) { + var rsvpDialog = new JK.RsvpCancelDialog(JK.app, sessionId, rsvpRequestId); rsvpDialog.initialize(); rsvpDialog.showDialog(); } diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index bcbb6d295..de05c6e48 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -7,6 +7,7 @@ context.JK = context.JK || {}; var logger = context.JK.logger; + var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR; var days = new Array("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"); @@ -15,6 +16,7 @@ "April", "May", "June", "July", "August", "September", "October", "November", "December"); + var os = null; context.JK.stringToBool = function (s) { switch (s.toLowerCase()) { @@ -85,9 +87,9 @@ $.each(context._.keys(icon_map_base), function (index, instrumentId) { var icon = icon_map_base[instrumentId]; - instrumentIconMap24[instrumentId] = "/assets/content/icon_instrument_" + icon + "24.png"; - instrumentIconMap45[instrumentId] = "/assets/content/icon_instrument_" + icon + "45.png"; - instrumentIconMap256[instrumentId] = "/assets/content/icon_instrument_" + icon + "256.png"; + instrumentIconMap24[instrumentId] = {asset: "/assets/content/icon_instrument_" + icon + "24.png", name: instrumentId}; + instrumentIconMap45[instrumentId] = {asset: "/assets/content/icon_instrument_" + icon + "45.png", name: instrumentId}; + instrumentIconMap256[instrumentId] = {asset: "/assets/content/icon_instrument_" + icon + "256.png", name: instrumentId}; }); /** @@ -96,12 +98,12 @@ * @param templateName the name of the help template (without the '#template-help' prefix). Add to _help.html.erb * @param data (optional) data for your template, if applicable * @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips - * */ context.JK.helpBubble = function ($element, templateName, data, options) { if (!data) { data = {} } + var helpText = context._.template($('#template-help-' + templateName).html(), data, { variable: 'data' }); var holder = $('
'); @@ -110,6 +112,37 @@ context.JK.hoverBubble($element, helpText, options); } + /** + * Associates a help bubble immediately with the specified $element, using jquery.bt.js (BeautyTips) + * By 'prod' it means to literally prod the user, to make them aware of something important because they did something else + * + * This will open a bubble immediately and show it for 4 seconds, + * if you call it again before the 4 second timer is up, it will renew the 4 second timer. + * @param $element The element that should show the help when hovered + * @param templateName the name of the help template (without the '#template-help' prefix). Add to _help.html.erb + * @param data (optional) data for your template, if applicable + * @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips + */ + context.JK.prodBubble = function($element, templateName, data, options) { + if(!options) options = {}; + options['trigger'] = 'none'; + options['clickAnywhereToClose'] = false + if(!options['duration']) options['duration'] = 4000; + + var existingTimer = $element.data("prodTimer"); + if(existingTimer) { + clearTimeout(existingTimer); + $element.btOn(); + } + else { + context.JK.helpBubble($element, templateName, data, options); + $element.btOn(); + } + $element.data("prodTimer", setTimeout(function() { + $element.data("prodTimer", null); + $element.btOff(); + }, options['duration'])); + } /** * Associates a bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips) * @param $element The element that should show the bubble when hovered @@ -345,26 +378,26 @@ context.JK.getInstrumentIcon24 = function (instrument) { if (instrument in instrumentIconMap24) { - return instrumentIconMap24[instrument]; + return instrumentIconMap24[instrument].asset; } - return instrumentIconMap24["default"]; + return instrumentIconMap24["default"].asset; }; context.JK.getInstrumentIcon45 = function (instrument) { if (instrument in instrumentIconMap45) { - return instrumentIconMap45[instrument]; + return instrumentIconMap45[instrument].asset; } - return instrumentIconMap45["default"]; + return instrumentIconMap45["default"].asset; }; context.JK.getInstrumentIcon256 = function (instrument) { if (instrument in instrumentIconMap256) { - return instrumentIconMap256[instrument]; + return instrumentIconMap256[instrument].asset; } - return instrumentIconMap256["default"]; + return instrumentIconMap256["default"].asset; }; // meant to pass in a bunch of images with an instrument-id attribute on them. @@ -493,6 +526,13 @@ return $item; } + context.JK.GetOSAsString = function() { + if(!os) { + os = context.jamClient.GetOSAsString(); + } + return os; + } + context.JK.search = function (query, app, callback) { //logger.debug("search: "+ query) $.ajax({ @@ -534,6 +574,111 @@ return keys; }; + context.JK.createProfileName = function(deviceInfo, chatName) { + var isSameInOut = deviceInfo.input.id == deviceInfo.output.id; + + var name = null; + if(isSameInOut) { + name = "In/Out: " + deviceInfo.input.info.displayName; + } + else { + name = "In: " + deviceInfo.input.info.displayName + ", Out: " + deviceInfo.output.info.displayName + } + + logger.debug("creating profile name: " + name); + return name; + } + + context.JK.selectedDeviceInfo = function(audioInputDeviceId, audioOutputDeviceId, deviceInformation) { + + if(!deviceInformation) { + deviceInformation = context.JK.loadDeviceInfo(); + } + + var input = deviceInformation[audioInputDeviceId]; + var output = deviceInformation[audioOutputDeviceId]; + + var inputBehavior = AUDIO_DEVICE_BEHAVIOR[input.type]; + var outputBehavior = AUDIO_DEVICE_BEHAVIOR[output.type]; + + return { + input: { + id: audioInputDeviceId, + info: input, + behavior: inputBehavior + }, + output: { + id: audioOutputDeviceId, + info: output, + behavior: outputBehavior + } + } + } + + context.JK.loadDeviceInfo = function() { + + var operatingSystem = context.JK.GetOSAsString(); + // should return one of: + // * MacOSX_builtin + // * MACOSX_interface + // * Win32_wdm + // * Win32_asio + // * Win32_asio4all + // * Linux + function determineDeviceType(deviceId, displayName) { + if (operatingSystem == "MacOSX") { + if (displayName.toLowerCase().trim() == "built-in") { + return "MacOSX_builtin"; + } + else { + return "MacOSX_interface"; + } + } + else if (operatingSystem == "Win32") { + if (context.jamClient.FTUEIsMusicDeviceWDM(deviceId)) { + return "Win32_wdm"; + } + else if (displayName.toLowerCase().indexOf("asio4all") > -1) { + return "Win32_asio4all" + } + else { + return "Win32_asio"; + } + } + else { + return "Linux"; + } + } + + var devices = context.jamClient.FTUEGetAudioDevices(); + logger.debug("FTUEGetAudioDevices: " + JSON.stringify(devices)); + + var loadedDevices = {}; + + // augment these devices by determining their type + context._.each(devices.devices, function (device) { + + if (device.name == "JamKazam Virtual Monitor") { + return; + } + + var deviceInfo = {}; + + deviceInfo.id = device.guid; + deviceInfo.type = determineDeviceType(device.guid, device.display_name); + deviceInfo.displayType = AUDIO_DEVICE_BEHAVIOR[deviceInfo.type].display; + deviceInfo.displayName = device.display_name; + deviceInfo.inputCount = device.input_count; + deviceInfo.outputCount = device.output_count; + + loadedDevices[device.guid] = deviceInfo; + }) + + logger.debug(context.JK.dlen(loadedDevices) + " devices loaded.", loadedDevices); + + return loadedDevices; + } + /** * Finds the first error associated with the field. * @param fieldName the name of the field @@ -656,8 +801,20 @@ } } + + // pass in 'arguments' in a fail callback of a $.ajax + context.JK.isNetworkError = function(failArgs) { + if(failArgs.length != 3) throw "expected 3 arguments from .fail of $ajax in isNetworkError" + var xhr = failArgs[0]; + + return xhr.status == 0; + } + context.JK.clientType = function () { - return context.jamClient.IsNativeClient() ? 'client' : 'browser'; + if (context.jamClient) { + return context.jamClient.IsNativeClient() ? 'client' : 'browser'; + } + return 'browser'; } /** * Returns 'MacOSX' if the os appears to be macintosh, @@ -726,7 +883,7 @@ } context.JK.checkbox = function ($checkbox) { - $checkbox.iCheck({ + return $checkbox.iCheck({ checkboxClass: 'icheckbox_minimal', radioClass: 'iradio_minimal', inheritClass: true diff --git a/web/app/assets/javascripts/web/scheduled_session.js b/web/app/assets/javascripts/web/scheduled_session.js index ff3792ede..ec86025cc 100644 --- a/web/app/assets/javascripts/web/scheduled_session.js +++ b/web/app/assets/javascripts/web/scheduled_session.js @@ -8,6 +8,7 @@ var logger = context.JK.logger; var rest = JK.Rest(); var ui = new context.JK.UIHelper(app); + var $btnAction = $("#btn-action"); function addComment(musicSessionId) { console.log("here"); @@ -56,7 +57,14 @@ }); } - function initComments(musicSessionId) { + function initialize(musicSessionId) { + registerScheduledSessionComment(); + + var $parent = $('.landing-sidebar'); + context.JK.bindHoverEvents($parent); + context.JK.setInstrumentAssetPath($('.instrument-icon', $parent)); + + // render comments $(".landing-comment-scroller").empty(); rest.getSessionHistory(musicSessionId) .done(function(response) { @@ -70,20 +78,37 @@ .fail(function(xhr) { }); - } - function initialize(musicSessionId) { - registerScheduledSessionComment(); + // retrieve RSVP requests for this user + rest.getRsvpRequests(musicSessionId) + .done(function(rsvps) { + if (rsvps && rsvps.length > 0) { + // should only be 1 RSVP for this session + var rsvp = rsvps[0]; + if (rsvp.canceled) { + $('.call-to-action').html('Your RSVP request to this session has been cancelled.'); + $btnAction.hide(); + } + else { + $('.call-to-action').html('Tell the session organizer if you can no longer join this session'); + $btnAction.html('CANCEL RSVP'); + $btnAction.click(function(e) { + ui.launchRsvpCancelDialog(musicSessionId, rsvp.id); + }); + } + } + // no RSVP + else { + $('.call-to-action').html("Tell the session organizer you'd like to play in this session"); + $btnAction.html('RSVP NOW!'); + $btnAction.click(function(e) { + ui.launchRsvpSubmitDialog(musicSessionId); + }); + } + }) + .fail(function(xhr) { - initComments(musicSessionId); - - var $parent = $('.landing-sidebar'); - context.JK.bindHoverEvents($parent); - context.JK.setInstrumentAssetPath($('.instrument-icon', $parent)); - - $("#btn-rsvp").click(function(e) { - ui.launchRsvpSubmitDialog(musicSessionId); - }); + }); $("#btnPostComment").click(function(e) { if ($.trim($("#txtSessionInfoComment").val()).length > 0) { @@ -92,6 +117,14 @@ $("#txtSessionComment").blur(); } }); + + $(document).on("rsvpSubmitEvent", function() { + location.reload(); + }); + + $(document).on("rsvpCancelEvent", function() { + location.reload(); + }); } this.initialize = initialize; diff --git a/web/app/assets/javascripts/wizard/gear/gear_wizard.js b/web/app/assets/javascripts/wizard/gear/gear_wizard.js new file mode 100644 index 000000000..69bbc9026 --- /dev/null +++ b/web/app/assets/javascripts/wizard/gear/gear_wizard.js @@ -0,0 +1,187 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.GearWizard = function (app) { + + var logger = context.JK.logger; + + var $dialog = null; + var wizard = null; + var $wizardSteps = null; + var $templateSteps = null; + + var self = this; + + var STEP_INTRO = 0; + var STEP_SELECT_DEVICE = 1; + var STEP_SELECT_TRACKS = 2; + var STEP_SELECT_CHAT = 3; + var STEP_DIRECT_MONITOR = 4; + var STEP_ROUTER_NETWORK = 5; + var STEP_SUCCESS = 6; + + var stepUnderstandGear = new context.JK.StepUnderstandGear(app, this); + var stepSelectGear = new context.JK.StepSelectGear(app, this); + var stepConfigureTracks = new context.JK.StepConfigureTracks(app, this); + var stepConfigureVoiceChat = new context.JK.StepConfigureVoiceChat(app, this); + var stepDirectMonitoring = new context.JK.StepDirectMonitoring(app, this); + var stepNetworkTest = new context.JK.StepNetworkTest(app, this); + var stepSuccess = new context.JK.StepSuccess(app, this); + + var STEPS = { + 0: stepUnderstandGear, + 1: stepSelectGear, + 2: stepConfigureTracks, + 3: stepConfigureVoiceChat, + 4: stepDirectMonitoring, + 5: stepNetworkTest, + 6: stepSuccess + } + + function newSession() { + context._.each(STEPS, function(stepInfo, stepNumber) { + if(stepInfo.newSession) { + stepInfo.newSession.call(stepInfo); + } + }); + } + + function onStepChanged(e, data) { + var step = wizard.getCurrentStep(); + var $currentWizardStep = wizard.getCurrentWizardStep(); + + // update ftue step numbers + var $ftueSteps = $(context._.template($templateSteps.html(), {}, { variable: 'data' })); + var $activeStep = $ftueSteps.find('.ftue-stepnumber[data-step-number="' + step + '"]'); + $activeStep.addClass('.active'); + $activeStep.next().show(); // show the .ftue-step-title + $currentWizardStep.find('.ftuesteps').replaceWith($ftueSteps); + } + + // checks if we already have a profile called 'FTUE...'; if not, create one. if so, re-use it. + function createFTUEProfile() { + var profileName = context.jamClient.FTUEGetMusicProfileName(); + + logger.debug("current profile name: " + profileName); + + if(profileName && profileName.indexOf('FTUE') == 0) { + // remove junk + context.jamClient.TrackDeleteProfile(profileName); + } + + var newProfileName = 'FTUEAttempt-' + new Date().getTime().toString(); + logger.debug("setting FTUE-prefixed profile name to: " + newProfileName); + context.jamClient.FTUESetMusicProfileName(newProfileName); + newSession(); + + var profileName = context.jamClient.FTUEGetMusicProfileName(); + + logger.debug("name on exit: " + profileName); + + } + + function beforeShow(args) { + + context.jamClient.FTUECancel(); + context.jamClient.FTUESetStatus(false); + createFTUEProfile(); + + wizard.onBeforeShow(args); + } + + function afterShow() { + + } + + function afterHide() { + wizard.onAfterHide(); + + context.jamClient.FTUESetStatus(true); + context.jamClient.FTUECancel(); + } + + + function onCanceled() { + if (app.cancelFtue) { + app.cancelFtue(); + app.afterFtue = null; + app.cancelFtue = null; + } + + return closeDialog(); + } + + function onClosed() { + if (app.afterFtue) { + // If there's a function to invoke, invoke it. + app.afterFtue(); + app.afterFtue = null; + app.cancelFtue = null; + } + + return closeDialog(); + } + + function closeDialog() { + wizard.onCloseDialog(); + app.layout.closeDialog('gear-wizard'); + return false; + } + + function events() { + $(wizard).on('step_changed', onStepChanged); + $(wizard).on('wizard_cancel', onCanceled); + $(wizard).on('wizard_close', onClosed); + } + + function setNextState(enabled) { + wizard.setNextState(enabled); + } + + function setBackState(enabled) { + wizard.setBackState(enabled); + } + + + function initialize() { + + // on initial page load, we are not in the FTUE. so cancel the FTUE and call FTUESetStatus(true) if needed + if(context.jamClient.FTUEGetStatus() == false) { + context.jamClient.FTUESetStatus(true); + } + context.jamClient.FTUECancel(); + + var dialogBindings = { beforeShow: beforeShow, afterShow: afterShow, afterHide: afterHide }; + + app.bindDialog('gear-wizard', dialogBindings); + + $dialog = $('#gear-wizard-dialog'); + $wizardSteps = $dialog.find('.wizard-step'); + $templateSteps = $('#template-ftuesteps'); + + stepUnderstandGear.initialize($wizardSteps.filter($('[layout-wizard-step=0]'))); + stepSelectGear.initialize($wizardSteps.filter($('[layout-wizard-step=1]'))); + stepConfigureTracks.initialize($wizardSteps.filter($('[layout-wizard-step=2]'))); + stepConfigureVoiceChat.initialize($wizardSteps.filter($('[layout-wizard-step=3]'))); + stepDirectMonitoring.initialize($wizardSteps.filter($('[layout-wizard-step=4]'))); + stepNetworkTest.initialize($wizardSteps.filter($('[layout-wizard-step=5]'))); + stepSuccess.initialize($wizardSteps.filter($('[layout-wizard-step=6]'))); + + wizard = new context.JK.Wizard(app); + wizard.initialize($dialog, $wizardSteps, STEPS); + + events(); + } + + this.setNextState = setNextState; + this.setBackState = setBackState; + this.initialize = initialize; + this.createFTUEProfile = createFTUEProfile; + + self = this; + return this; + }; + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/wizard/gear/step_configure_tracks.js b/web/app/assets/javascripts/wizard/gear/step_configure_tracks.js new file mode 100644 index 000000000..cfcabc988 --- /dev/null +++ b/web/app/assets/javascripts/wizard/gear/step_configure_tracks.js @@ -0,0 +1,270 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepConfigureTracks = function (app) { + + var ASSIGNMENT = context.JK.ASSIGNMENT; + var VOICE_CHAT = context.JK.VOICE_CHAT; + var MAX_TRACKS = context.JK.MAX_TRACKS; + + var logger = context.JK.logger; + + var $step = null; + var $templateAssignablePort = null; + var $templateTrackTarget = null; + var $unassignedChannelsHolder = null; + var $tracksHolder = null; + var $instrumentsHolder = null; + + + function loadChannels() { + var musicPorts = jamClient.FTUEGetChannels(); + + $unassignedChannelsHolder.empty(); + $tracksHolder.find('.ftue-input').remove(); + + var inputChannels = musicPorts.inputs; + + context._.each(inputChannels, function (inputChannel) { + if(inputChannel.assignment == ASSIGNMENT.UNASSIGNED) { + var $channel = $(context._.template($templateAssignablePort.html(), inputChannel, { variable: 'data' })); + unassignChannel($channel); + } + else { + var $channel = $(context._.template($templateAssignablePort.html(), inputChannel, { variable: 'data' })); + + // find the track this belongs in + + var trackNumber = inputChannel.assignment - 1; + + var $track = $tracksHolder.find('.track[data-num="' + trackNumber + '"]') + + if($track.length == 0) { + context.JK.alertSupportedNeeded('Unable to find a track for channel with assignment ' + inputChannel.assignment); + return false; + } + addChannelToTrack($channel, $track.find('.track-target')); + } + + $channel.draggable({ + helper: 'clone', + start: function() { + var $channel = $(this); + var $track = $channel.closest('.track-target'); + var isUnassigned = $track.length == 0; + if(isUnassigned) { + $tracksHolder.find('.track-target').addClass('possible-target'); + } + else { + $tracksHolder.find('.track-target').addClass('possible-target'); + $unassignedChannelsHolder.addClass('possible-target'); + } + }, + stop: function() { + $tracksHolder.find('.track-target').removeClass('possible-target'); + $unassignedChannelsHolder.removeClass('possible-target') + } + }); + }) + } + + // iterates through the dom and returns a pure data structure for track associations + function trackAssociations() { + + var tracks = {}; + tracks.tracks = []; + tracks.unassignedChannels = []; + var $unassignedChannels = $unassignedChannelsHolder.find('.ftue-input'); + var $tracks = $tracksHolder.find('.track-target'); + + context._.each($unassignedChannels, function($unassignedTrack) { + $unassignedTrack = $($unassignedTrack); + var channelId = $unassignedTrack.attr('data-input-id'); + tracks.unassignedChannels.push(channelId); + }) + + context._.each($tracks, function($track, index) { + $track = $($track); + var $assignedChannels = $track.find('.ftue-input'); + + var track = {index: index, channels:[]}; + context._.each($assignedChannels, function($assignedChannel) { + $assignedChannel = $($assignedChannel); + track.channels.push($assignedChannel.attr('data-input-id')) + }); + + // sparse array + if(track.channels.length > 0) { + tracks.tracks.push(track); + } + var $instrument = $instrumentsHolder.find('[data-num="' + index + '"]').find('.icon-instrument-select'); + track.instrument_id = $instrument.data('instrument_id'); + }) + return tracks; + } + + function validate(tracks) { + // there must be at least one assigned channel + + if(tracks.tracks.length == 0) { + logger.debug("ConfigureTracks validation error: must have assigned at least one input port to a track."); + context.JK.Banner.showAlert('Must have assigned at least one input port to a track.'); + return false; + } + + context._.each(tracks.tracks, function(track) { + if(!track.instrument_id) { + logger.debug("ConfigureTracks validation error: all tracks with ports assigned must specify an instrument."); + context.JK.Banner.showAlert('All tracks with ports assigned must specify an instrument.'); + return false; + } + }); + + return true; + } + + function save(tracks) { + + context._.each(tracks.unassignedChannels, function(unassignedChannelId) { + context.jamClient.TrackSetAssignment(unassignedChannelId, true, ASSIGNMENT.UNASSIGNED); + }); + + context._.each(tracks.tracks, function(track, index) { + + var trackNumber = index + 1; + + context._.each(track.channels, function(channelId) { + context.jamClient.TrackSetAssignment(channelId, true, trackNumber); + + }); + logger.debug("context.jamClient.TrackSetInstrument(trackNumber, track.instrument_id)", trackNumber, track.instrument_id); + context.jamClient.TrackSetInstrument(trackNumber, context.JK.instrument_id_to_instrument[track.instrument_id].client_id); + }); + + var result = context.jamClient.TrackSaveAssignments(); + + if(!result || result.length == 0) { + // success + return true; + } + else { + context.JK.Banner.showAlert('Unable to save assignments. ' + result); + return false; + } + } + + function loadTrackInstruments() { + var $trackInstruments = $instrumentsHolder.find('.track-instrument'); + + context._.each($trackInstruments, function(trackInstrument) { + var $trackInstrument = $(trackInstrument); + + var trackIndex = parseInt($trackInstrument.attr('data-num')) + 1; + + var clientInstrument = context.jamClient.TrackGetInstrument(trackIndex); + + var instrument = context.JK.client_to_server_instrument_map[clientInstrument]; + + $trackInstrument.instrumentSelectorSet(instrument ? instrument.server_id : instrument); + }); + } + + function handleNext() { + var tracks = trackAssociations(); + + if(!validate(tracks)) { + return false; + } + + var saved = save(tracks); + + if(saved) { + context.JK.GA.trackConfigureTracksCompletion(context.JK.detectOS()); + } + + return saved; + } + + function beforeShow() { + loadChannels(); + loadTrackInstruments(); + } + + function unassignChannel($channel) { + var $originallyAssignedTrack = $channel.closest('.track-target'); + $unassignedChannelsHolder.append($channel); + $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length); + + } + function addChannelToTrack($channel, $track) { + var $originallyAssignedTrack = $channel.closest('.track-target'); + $track.append($channel); + $track.attr('track-count', $track.find('.ftue-input:not(.ui-draggable-dragging)').length); + $originallyAssignedTrack.attr('track-count', $originallyAssignedTrack.find('.ftue-input:not(.ui-draggable-dragging)').length) + } + + function initializeUnassignedDroppable() { + $unassignedChannelsHolder.droppable( + { + activeClass: 'drag-in-progress', + hoverClass: 'drag-hovering', + drop: function( event, ui ) { + var $channel = ui.draggable; + //$channel.css('left', '0').css('top', '0'); + unassignChannel($channel); + } + }); + } + + function initializeTrackDroppables() { + var i; + for(i = 0; i < MAX_TRACKS; i++) { + var $target = $(context._.template($templateTrackTarget.html(), {num: i }, { variable: 'data' })); + $tracksHolder.append($target); + $target.find('.track-target').droppable( + { + activeClass: 'drag-in-progress', + hoverClass: 'drag-hovering', + drop: function( event, ui ) { + var $track = $(this); + var $channel = ui.draggable; + //$channel.css('left', '0').css('top', '0'); + addChannelToTrack($channel, $track); + } + }); + } + } + + function initializeInstrumentDropdown() { + var i; + for(i = 0; i < MAX_TRACKS; i++) { + var $root = $('
'); + $root.instrumentSelector().attr('data-num', i); + $instrumentsHolder.append($root); + } + } + + function initialize(_$step) { + $step = _$step; + + $templateAssignablePort = $('#template-assignable-port'); + $templateTrackTarget = $('#template-track-target'); + $unassignedChannelsHolder = $step.find('.unassigned-channels'); + $tracksHolder = $step.find('.tracks'); + $instrumentsHolder = $step.find('.instruments'); + + + initializeUnassignedDroppable(); + initializeTrackDroppables(); + initializeInstrumentDropdown(); + } + + this.handleNext = handleNext; + this.beforeShow = beforeShow; + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/wizard/gear/step_configure_voice_chat.js b/web/app/assets/javascripts/wizard/gear/step_configure_voice_chat.js new file mode 100644 index 000000000..fddc56cf0 --- /dev/null +++ b/web/app/assets/javascripts/wizard/gear/step_configure_voice_chat.js @@ -0,0 +1,166 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepConfigureVoiceChat = function (app) { + + var ASSIGNMENT = context.JK.ASSIGNMENT; + var VOICE_CHAT = context.JK.VOICE_CHAT; + var logger = context.JK.logger; + + var $step = null; + var $reuseAudioInputRadio = null; + var $useChatInputRadio = null; + var $chatInputs = null; + var $templateChatInput = null; + var $selectedChatInput = null;// should only be used if isChatEnabled = true + + function newSession() { + $reuseAudioInputRadio.attr('checked', 'checked').iCheck('check'); + } + + function isChannelAvailableForChat(chatChannelId, musicPorts) { + var result = true; + context._.each(musicPorts.input, function(inputChannel) { + // if the channel is currently assigned to a track, it not unassigned + if(inputChannel.id == chatChannelId && (inputChannel.assignment > 0)) { + result = false; + return false; // break + } + }); + + return result; + } + + function isChatEnabled() { + return $useChatInputRadio.is(':checked'); + } + + function beforeShow() { + + if(isChatEnabled()) { + enableChat(); + } + else { + disableChat(); + } + + var musicPorts = jamClient.FTUEGetChannels(); + var chatInputs = context.jamClient.FTUEGetChatInputs(); + + $chatInputs.empty(); + + context._.each(chatInputs, function(chatChannelName, chatChannelId) { + if(isChannelAvailableForChat(chatChannelId, musicPorts)) { + var $chat = $(context._.template($templateChatInput.html(), {id: chatChannelId, name: chatChannelName}, { variable: 'data' })); + $chat.hide(); // we'll show it once it's styled with iCheck + $chatInputs.append($chat); + } + }); + + var $radioButtons = $chatInputs.find('input[name="chat-device"]'); + context.JK.checkbox($radioButtons).on('ifChecked', function(e) { + var $input = $(e.currentTarget); + $selectedChatInput = $input; // for use in handleNext + var channelId = $input.attr('data-channel-id'); + context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.CHAT); + var result = context.jamClient.TrackSaveAssignments(); + + if(!result || result.length == 0) { + // success + } + else { + context.JK.Banner.showAlert('Unable to save assignments. ' + result); + return false; + } + }); + + if(!isChatEnabled()) { + $radioButtons.iCheck('disable'); + } + + $chatInputs.find('.chat-input').show().on('click', function() { + if(!isChatEnabled()) { + context.JK.prodBubble($step.find('.use-chat-input h3'), 'chat-not-enabled', {}, { positions:['left']}); + } + }) + } + + function disableChat() { + logger.debug("FTUE: disabling chat"); + context.jamClient.TrackSetChatEnable(false); + var result = context.jamClient.TrackSaveAssignments(); + + if(!result || result.length == 0) { + // success + } + else { + context.JK.Banner.showAlert('Unable to disable chat. ' + result); + return false; + } + + var $radioButtons = $chatInputs.find('input[name="chat-device"]'); + $radioButtons.iCheck('disable'); + } + + function enableChat() { + logger.debug("FTUE: enabling chat"); + context.jamClient.TrackSetChatEnable(true); + var result = context.jamClient.TrackSaveAssignments(); + + if(!result || result.length == 0) { + // success + } + else { + context.JK.Banner.showAlert('Unable to enable chat. ' + result); + return false; + } + + var $radioButtons = $chatInputs.find('input[name="chat-device"]'); + $radioButtons.iCheck('enable'); + } + + function handleChatEnabledToggle() { + context.JK.checkbox($reuseAudioInputRadio); + context.JK.checkbox($useChatInputRadio); + + // plugin sets to relative on the element; have to do this as an override + $reuseAudioInputRadio.closest('.iradio_minimal').css('position', 'absolute'); + $useChatInputRadio.closest('.iradio_minimal').css('position', 'absolute'); + + $reuseAudioInputRadio.on('ifChecked', disableChat); + $useChatInputRadio.on('ifChecked', enableChat) + } + + function handleNext() { + var selectedDeviceInfo = context.JK.selectedDeviceInfo(context.jamClient.FTUEGetInputMusicDevice(), context.jamClient.FTUEGetOutputMusicDevice()); + + var chatName = null; + if(isChatEnabled()) { + chatName = $selectedChatInput.attr('data-channel-name'); + } + context.jamClient.FTUESetMusicProfileName(context.JK.createProfileName(selectedDeviceInfo, chatName)); + + return true; + } + + function initialize(_$step) { + $step = _$step; + + $reuseAudioInputRadio = $step.find('.reuse-audio-input input'); + $useChatInputRadio = $step.find('.use-chat-input input'); + $chatInputs = $step.find('.chat-inputs'); + $templateChatInput = $('#template-chat-input'); + + handleChatEnabledToggle(); + } + + this.handleNext = handleNext; + this.newSession = newSession; + this.beforeShow = beforeShow; + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/wizard/gear/step_direct_monitoring.js b/web/app/assets/javascripts/wizard/gear/step_direct_monitoring.js new file mode 100644 index 000000000..c1cb4f514 --- /dev/null +++ b/web/app/assets/javascripts/wizard/gear/step_direct_monitoring.js @@ -0,0 +1,95 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepDirectMonitoring = function (app) { + + var $step = null; + var $directMonitoringBtn = null; + var isPlaying = false; + var playCheckInterval = null; + var trackDurationMs = null; + + function checkIfPlaying() { + var currentPositionMs = context.jamClient.SessionCurrrentPlayPosMs(); + var atEnd = currentPositionMs == 0 || trackDurationMs == currentPositionMs; + + if(atEnd) { + context.jamClient.SessionStopPlay(); + startPlay(); + } + } + + function startPlay() { + context.jamClient.SessionTrackSeekMs(0); + context.jamClient.SessionStartPlay(1); + $directMonitoringBtn.removeClass('playing paused').addClass('playing'); + + trackDurationMs = context.jamClient.SessionGetTracksPlayDurationMs(); + if(!playCheckInterval) { + playCheckInterval = setInterval(checkIfPlaying, 333); + } + isPlaying = true; + } + + function stopPlay() { + context.jamClient.SessionStopPlay(); + $directMonitoringBtn.removeClass('playing paused').addClass('paused'); + isPlaying = false; + } + + function togglePlay() { + if(isPlaying) { + stopPlay(); + } + else { + startPlay(); + } + } + + function handleNext() { + + } + + function newSession() { + + } + + function beforeShow() { + context.jamClient.SessionRemoveAllPlayTracks(); + if(!context.jamClient.SessionAddPlayTrack("skin:jktest-audio.wav")) { + context.JK.alertSupportedNeeded('Unable to open test sound'); + } + } + + function beforeHide() { + if(isPlaying) { + stopPlay(); + } + + context.jamClient.SessionRemoveAllPlayTracks(); + + if(playCheckInterval) { + clearTimeout(playCheckInterval); + playCheckInterval = null; + } + } + + function initialize(_$step) { + $step = _$step; + + $directMonitoringBtn = $step.find('.test-direct-monitoring'); + + $directMonitoringBtn.on('click', togglePlay); + } + + this.handleNext = handleNext; + this.newSession = newSession; + this.beforeShow = beforeShow; + this.beforeHide = beforeHide; + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/wizard/gear/step_network_test.js b/web/app/assets/javascripts/wizard/gear/step_network_test.js new file mode 100644 index 000000000..2669d0289 --- /dev/null +++ b/web/app/assets/javascripts/wizard/gear/step_network_test.js @@ -0,0 +1,466 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepNetworkTest = function (app, $dialog) { + + var NETWORK_TEST_TYPES = { + RestPhase : 0, + PktTest100NormalLatency : 1, + PktTest200MediumLatency : 2, + PktTest400LowLatency : 3, + PktTestRateSweep : 4, + RcvOnly : 5 + } + var STARTING_NUM_CLIENTS = 4; + var PAYLOAD_SIZE = 100; + var MINIMUM_ACCEPTABLE_SESSION_SIZE = 2; + + var rest = context.JK.Rest(); + var logger = context.JK.logger; + var $step = null; + var TEST_SUCCESS_CALLBACK = 'JK.HandleNetworkTestSuccess'; + var TEST_TIMEOUT_CALLBACK = 'JK.HandleNetworkTestTimeout'; + + var $startNetworkTestBtn = null; + var $testResults = null; + var $testScore = null; + var $testText = null; + var testedSuccessfully = false; + var $scoringBar = null; + var $goodMarker = null; + var $goodLine = null; + var $currentScore = null; + var $scoredClients = null; + var $subscore = null; + var backendGuardTimeout = null; + + var serverClientId = ''; + var isScoring = false; + var numClientsToTest = STARTING_NUM_CLIENTS; + var testSummary = {attempts : [], final:null} + + var scoringZoneWidth = 100;//px + + + function reset() { + serverClientId = ''; + isScoring = false; + numClientsToTest = STARTING_NUM_CLIENTS; + testSummary = {attempts : []}; + updateControlsState(); + } + + function renderStartTest() { + $scoredClients.empty(); + $testResults.removeClass('good acceptable bad').addClass('testing'); + $testText.empty(); + $subscore.empty(); + updateControlsState(); + $currentScore.width(0); + $goodLine.css('left', (gon.ftue_packet_rate_treshold * 100) + '%'); + $goodMarker.css('left', (gon.ftue_packet_rate_treshold * 100) + '%'); + } + + function renderStopTest(score, text) { + $scoredClients.html(score); + $testText.html(text); + $testResults.removeClass('testing'); + } + + function postDiagnostic() { + rest.createDiagnostic({ + type: 'NETWORK_TEST_RESULT', + data: {client_type: context.JK.clientType(), client_id: context.JK.JamServer.clientID, summary:testSummary} + }); + } + + function testFinished() { + var attempt = getCurrentAttempt(); + + if(!testSummary.final) { + testSummary.final = {reason : attempt.reason}; + } + + var reason = testSummary.final.reason; + + if(reason == "success") { + renderStopTest(attempt.num_clients, "Your router and Internet service will support sessions of up to " + attempt.num_clients + " JamKazam musicians.") + testedSuccessfully = true; + if(!testSummary.final.num_clients) { + testSummary.final.num_clients = attempt.num_clients; + } + context.JK.GA.trackNetworkTest(context.JK.detectOS(), testSummary.final.num_clients); + context.jamClient.SetNetworkTestScore(attempt.num_clients); + if(testSummary.final.num_clients == 2) { + $testResults.addClass('acceptable'); + } + else { + $testResults.addClass('good'); + } + } + else if(reason == "minimum_client_threshold") { + context.jamClient.SetNetworkTestScore(0); + renderStopTest('', "We're sorry, but your router and Internet service will not effectively support JamKazam sessions. Please click the HELP button for more information.") + } + else if(reason == "unreachable") { + context.jamClient.SetNetworkTestScore(0); + renderStopTest('', "We're sorry, but your router will not support JamKazam in its current configuration. Please click the HELP button for more information."); + } + else if(reason == "internal_error") { + context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection."); + renderStopTest('', ''); + } + else if(reason == "remote_peer_cant_test") { + context.JK.alertSupportedNeeded("The JamKazam service is experiencing technical difficulties."); + renderStopTest('', ''); + } + else if(reason == 'backend_gone') { + context.JK.alertSupportedNeeded("The JamKazam client is experiencing technical difficulties."); + renderStopTest('', ''); + } + else if(reason == "invalid_response") { + context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection. Reason=" + attempt.backend_data.reason + '.'); + renderStopTest('', ''); + } + else if(reason == 'no_servers') { + context.JK.alertSupportedNeeded("No network test servers are available. You can skip this step for now."); + renderStopTest('', ''); + testedSuccessfully = true; + } + else if(reason == 'no_network') { + context.JK.Banner.showAlert("Please try again later. Your network appears down."); + renderStopTest('', ''); + } + else if(reason == "rest_api_error") { + context.JK.alertSupportedNeeded("Unable to acquire a network test server. You can skip this step for now."); + testedSuccessfully = true; + renderStopTest('', ''); + } + else if(reason == "timeout") { + context.JK.alertSupportedNeeded("Communication with a network test servers timed out. You can skip this step for now."); + testedSuccessfully = true; + renderStopTest('', ''); + } + else { + context.JK.alertSupportedNeeded("The JamKazam client software had a logic error while scoring your Internet connection."); + renderStopTest('', ''); + } + + numClientsToTest = STARTING_NUM_CLIENTS; + isScoring = false; + updateControlsState(); + postDiagnostic(); + } + + function getCurrentAttempt() { + return testSummary.attempts[testSummary.attempts.length - 1]; + } + + function backendTimedOut() { + testSummary.final = {reason: 'backend_gone'} + testFinished(); + } + + function clearBackendGuard() { + if(backendGuardTimeout) { + clearTimeout(backendGuardTimeout); + backendGuardTimeout = null; + } + } + + function attemptTestPass() { + + var attempt = {}; + attempt.payload_size = PAYLOAD_SIZE; + attempt.duration = gon.ftue_network_test_duration; + attempt.test_type = 'PktTest400LowLatency'; + attempt.num_clients = numClientsToTest; + attempt.server_client_id = serverClientId; + attempt.received_progress = false; + testSummary.attempts.push(attempt); + + //context.jamClient.StopNetworkTest(''); + + $testText.text("Simulating the network traffic of a " + numClientsToTest + "-person session."); + + updateProgress(0, false); + + backendGuardTimeout = setTimeout(function(){backendTimedOut()}, (gon.ftue_network_test_duration + 1) * 1000); + + context.jamClient.TestNetworkPktBwRate(serverClientId, TEST_SUCCESS_CALLBACK, TEST_TIMEOUT_CALLBACK, + NETWORK_TEST_TYPES.PktTest400LowLatency, + gon.ftue_network_test_duration, + numClientsToTest, + PAYLOAD_SIZE); + } + + + function startNetworkTest(checkWireless) { + + if(isScoring) return false; + + if(checkWireless) { + // check if on Wifi 1st + var isWireless = context.jamClient.IsMyNetworkWireless(); + if(isWireless == -1) { + logger.warn("unable to determine if the user is on wireless or not for network test. skipping prompt.") + } + else if(isWireless == 1) { + context.JK.Banner.showAlert({buttons: [ + {name: 'RUN NETWORK TEST ANYWAY', click: function() {startNetworkTest(false)}}, + {name: 'CANCEL', click: function() {}}], + html: "

It appears that your computer is connected to your network using WiFi.

" + + "

We strongly advise against running the JamKazam application on a WiFi connection. " + + "We recommend using a wired Ethernet connection from your computer to your router. " + + "A WiFi connection is likely to cause significant issues in both latency and audio quality.

"}) + return false; + } + } + + reset(); + isScoring = true; + renderStartTest(); + rest.getLatencyTester() + .done(function(response) { + // ensure there are no tests ongoing + + serverClientId = response.client_id; + + logger.info("beginning network test against client_id: " + serverClientId); + + attemptTestPass(); + }) + .fail(function(jqXHR) { + if(jqXHR.status == 404) { + // means there are no network testers available. + // we have to skip this part of the UI + testSummary.final = {reason: 'no_servers'} + } + else { + if(context.JK.isNetworkError(arguments)) { + testSummary.final = {reason: 'no_network'} + } + else { + testSummary.final = {reason: 'rest_api_error'} + } + } + testFinished(); + }) + logger.info("starting network test"); + return false; + } + + function updateProgress(throughput, showSubscore) { + var width = throughput * 100; + + $currentScore.stop().data('showSubscore', showSubscore); + + if(!showSubscore) { + $subscore.text(''); + } + + $currentScore.animate({ + duration: 1000, + width: width + '%' + }, { + step: function (now, fx) { + if(showSubscore) { + var newWidth = ( 100 * parseFloat($currentScore.css('width')) / parseFloat($currentScore.parent().css('width')) ); + $subscore.text((Math.round(newWidth * 10) / 10) + '%'); + } + } + }).css('overflow', 'visible'); + ; + } + + function networkTestSuccess(data) { + clearBackendGuard(); + + var attempt = getCurrentAttempt(); + + function refineTest(up) { + if(up) { + if(numClientsToTest == gon.ftue_network_test_max_clients) { + attempt.reason = "success"; + testFinished(); + } + else { + numClientsToTest++; + logger.debug("increasing number of clients to " + numClientsToTest); + setTimeout(attemptTestPass, 500); // wait a second to avoid race conditions with client/server comm + } + } + else { + // reduce numclients if we can + if(numClientsToTest == MINIMUM_ACCEPTABLE_SESSION_SIZE) { + // we are too low already. fail the user + attempt.reason = "minimum_client_threshold"; + testFinished(); + } + else if(numClientsToTest > STARTING_NUM_CLIENTS) { + // this means we've gone up before... so don't go back down (i.e., creating a loop) + attempt.reason = "success"; + testSummary.final = { reason:'success', num_clients: numClientsToTest - 1 } + testFinished(); + } + else { + numClientsToTest--; + logger.debug("reducing number of clients to " + numClientsToTest); + setTimeout(attemptTestPass, 500); // wait a second to avoid race conditions with client/server comm + } + } + } + + attempt.backend_data = data; + + if(data.progress === true) { + + var animate = true; + if(data.downthroughput && data.upthroughput) { + + if(data.downthroughput > 0 || data.upthroughput > 0) { + attempt.received_progress = true; + animate = true; + } + + if(attempt.received_progress) { + // take the lower + var throughput= data.downthroughput < data.upthroughput ? data.downthroughput : data.upthroughput; + + updateProgress(throughput, true); + } + } + } + else { + + logger.debug("network test pass success. data: ", data); + + if(data.reason == "unreachable") { + // STUN + logger.debug("network test: unreachable (STUN issue or similar)"); + attempt.reason = data.reason; + testFinished(); + } + else if(data.reason == "internal_error") { + // oops + logger.debug("network test: internal_error (client had a unexpected problem)"); + attempt.reason = data.reason; + testFinished(); + } + else if(data.reason == "remote_peer_cant_test") { + // old client + logger.debug("network test: remote_peer_cant_test (old client)") + attempt.reason = data.reason; + testFinished(); + } + else { + if(!data.downthroughput || !data.upthroughput) { + // we have to assume this is bad. just not a reason we know about in code + logger.debug("network test: no test data (unknown issue? " + data.reason + ")") + attempt.reason = "invalid_response"; + testFinished(); + } + else { + // success... but we still have to verify if this data is within threshold + if(data.downthroughput < gon.ftue_packet_rate_treshold) { + logger.debug("network test: downthroughput too low. downthroughput: " + data.downthroughput + ", threshold: " + gon.ftue_packet_rate_treshold); + refineTest(false); + } + else if(data.upthroughput < gon.ftue_packet_rate_treshold) { + logger.debug("network test: upthroughput too low. upthroughput: " + data.upthroughput + ", threshold: " + gon.ftue_packet_rate_treshold); + refineTest(false); + } + else { + // true success. we can accept this score + logger.debug("network test: success") + refineTest(true); + } + } + } + + // VRFS-1742 + // context.jamClient.StopNetworkTest(serverClientId); + } + + } + + function networkTestTimeout(data) { + clearBackendGuard(); + + logger.warn("network timeout when testing latency test: " + data); + + var attempt = getCurrentAttempt(); + attempt.reason = 'timeout'; + attempt.backend_data = data; + testFinished(); + } + + function hasScoredNetworkSuccessfully() { + return testedSuccessfully; + } + + function configureStartButton() { + if(isScoring) { + $startNetworkTestBtn.text('NETWORK TEST RUNNING...').removeClass('button-orange').addClass('button-grey') + } + else { + $startNetworkTestBtn.text('START NETWORK TEST').removeClass('button-grey').addClass('button-orange'); + } + + } + function updateControlsState() { + initializeNextButtonState(); + initializeBackButtonState(); + configureStartButton(); + } + + function initializeNextButtonState() { + $dialog.setNextState(hasScoredNetworkSuccessfully() && !isScoring); + } + + function initializeBackButtonState() { + $dialog.setBackState(!isScoring); + } + + function newSession() { + reset(); + //context.jamClient.StopNetworkTest(''); + } + + function beforeShow() { + reset(); + } + + function beforeHide() { + clearBackendGuard(); + } + + function initialize(_$step) { + $step = _$step; + + $startNetworkTestBtn = $step.find('.start-network-test'); + $testResults = $step.find('.network-test-results'); + $testScore = $step.find('.network-test-score'); + $testText = $step.find('.network-test-text'); + $scoringBar = $step.find('.scoring-bar'); + $goodMarker = $step.find('.good-marker'); + $goodLine =$step.find('.good-line'); + $currentScore = $step.find('.current-score'); + $scoredClients= $step.find('.scored-clients'); + $subscore = $step.find('.subscore'); + $startNetworkTestBtn.on('click', startNetworkTest); + } + + this.newSession = newSession; + this.beforeHide = beforeHide; + this.beforeShow = beforeShow; + this.initialize = initialize; + + context.JK.HandleNetworkTestSuccess = networkTestSuccess; // pin to global for bridge callback + context.JK.HandleNetworkTestTimeout = networkTestTimeout; // pin to global for bridge callback + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/wizard/gear/step_select_gear.js b/web/app/assets/javascripts/wizard/gear/step_select_gear.js new file mode 100644 index 000000000..a254f6c07 --- /dev/null +++ b/web/app/assets/javascripts/wizard/gear/step_select_gear.js @@ -0,0 +1,1171 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepSelectGear = function (app, $dialog) { + + var ASSIGNMENT = context.JK.ASSIGNMENT; + var VOICE_CHAT = context.JK.VOICE_CHAT; + var AUDIO_DEVICE_BEHAVIOR = context.JK.AUDIO_DEVICE_BEHAVIOR; + + var self = null; + var $step = null; + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var $watchVideoInput = null; + var $watchVideoOutput = null; + var $audioInput = null; + var $audioOutput = null; + var $bufferIn = null; + var $bufferOut = null; + var $frameSize = null; + var $inputChannels = null; + var $outputChannels = null; + var $knobs = null; + var $scoreReport = null; + var $latencyScoreSection = null; + var $latencyScore = null; + var $latencyHeader = null; + var $ioHeader = null; + var $ioScoreSection = null; + var $ioRate = null; + var $ioRateScore = null; + var $ioVar = null; + var $ioVarScore = null; + var $ioCountdown = null; + var $ioCountdownSecs = null; + var $resultsText = null; + var $unknownText = null; + var $asioInputControlBtn = null; + var $asioOutputControlBtn = null; + var $resyncBtn = null; + var $templateAudioPort = null; + var $launchLoopbackBtn = null; + var $instructions = null; + + var operatingSystem = null; + var iCheckIgnore = false; + var scoring = false; // are we currently scoring + var validDevice = false; // do we currently have a device selected that we can score against? + + // cached values between + var deviceInformation = null; + var lastSelectedDeviceInfo = null; + var shownOutputProdOnce = false; + var shownInputProdOnce = false; + + var selectedDeviceInfo = null; + var musicPorts = null; + var validLatencyScore = false; + var validIOScore = false; + var lastLatencyScore = null; + var ioScore = null; + var latencyScore = null; + + var savedProfile = false; + + + + function isGoodFtue() { + return validLatencyScore && validIOScore; + } + + // returns a deviceInfo hash for the device matching the deviceId, or undefined. + function findDevice(deviceId) { + return deviceInformation[deviceId]; + } + + function selectedAudioInput() { + return $audioInput.val(); + } + + function selectedAudioOutput() { + return $audioOutput.val(); + } + + function selectedFramesize() { + return parseFloat($frameSize.val()); + } + + function selectedBufferIn() { + return parseFloat($bufferIn.val()); + } + + function selectedBufferOut() { + return parseFloat($bufferOut.val()); + } + + function setFramesize(value) { + context.JK.dropdown($frameSize.val(value).easyDropDown('select', value.toString(), true)) + } + + function setBufferIn(value) { + context.JK.dropdown($bufferIn.val(value).easyDropDown('select', value.toString(), true)); + } + + function setBufferOut(value) { + context.JK.dropdown($bufferOut.val(value).easyDropDown('select', value.toString(), true)) + } + + function setInputAudioDevice(value) { + context.JK.dropdown($audioInput.val(value).easyDropDown('select', value.toString(), true)) + } + + function setOutputAudioDevice(value) { + context.JK.dropdown($audioOutput.val(value).easyDropDown('select', value.toString(), true)) + } + + function initializeNextButtonState() { + $dialog.setNextState(isGoodFtue()); + } + + function initializeBackButtonState() { + $dialog.setBackState(!scoring); + } + + function initializeAudioInput() { + var optionsHtml = ''; + optionsHtml = ''; + context._.each(deviceInformation, function (deviceInfo, deviceId) { + if(deviceInfo.inputCount > 0) { + optionsHtml += ''; + } + }); + + console.log("INITIALIZE AUDIO INPUT: " + optionsHtml) + $audioInput.html(optionsHtml); + context.JK.dropdown($audioInput); + $audioInput.easyDropDown('enable') + + initializeAudioInputChanged(); + } + + function initializeAudioOutput() { + var optionsHtml = ''; + optionsHtml = ''; + context._.each(deviceInformation, function (deviceInfo, deviceId) { + if(deviceInfo.outputCount > 0) { + optionsHtml += ''; + } + }); + $audioOutput.html(optionsHtml); + context.JK.dropdown($audioOutput); + $audioOutput.easyDropDown('disable'); // enable once they pick something in input + + initializeAudioOutputChanged(); + } + + function initializeFramesize() { + context.JK.dropdown($frameSize); + } + + function initializeBuffers() { + context.JK.dropdown($bufferIn); + context.JK.dropdown($bufferOut); + } + + + // reloads the backend's channel state for the currently selected audio devices, + // and update's the UI accordingly + function initializeChannels() { + musicPorts = jamClient.FTUEGetChannels(); + + initializeInputPorts(musicPorts); + initializeOutputPorts(musicPorts); + } + + // select 2 (or 1) inputs and 2 outputs for the user. required to get a latency score + // also, arguably convenient + function autoSelectMinimumValidChannels() { + + var audioInputDeviceId = selectedAudioInput(); + var audioOutputDeviceId = selectedAudioOutput(); + + var $allInputs = $inputChannels.find('input[type="checkbox"]'); + + if ($allInputs.length == 0) { + // ERROR: not enough channels + if(!audioInputDeviceId || audioInputDeviceId == '') { + context.JK.prodBubble($audioInput.closest('.easydropdown-wrapper'), 'select-input', {}, {positions:['right', 'top']}); + } + //context.JK.Banner.showAlert('To be a valid input audio device, the device must have at least 1 input channel.'); + return false; + } + + var $allOutputs = $outputChannels.find('input[type="checkbox"]'); + if ($allOutputs.length < 2) { + if(!audioOutputDeviceId || audioOutputDeviceId == '') { + context.JK.prodBubble($audioOutput.closest('.easydropdown-wrapper'), 'select-output', {}, {positions:['right', 'top'], duration:7000}); + } + // ERROR: not enough channels + //context.JK.Banner.showAlert('To be a valid output audio device, the device must have at least 2 output channels.'); + return false; + } + + // ensure 1, or preferably 2, input channels are selected + var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked'); + var $unassignedInputs = $inputChannels.find('input[type="checkbox"]:not(:checked)'); + if ($assignedInputs.length == 0) { + if ($allInputs.length >= 2) { + logger.debug("selecting 2 inputs") + $unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked'); + // this is required because iCheck change handler re-writes the inputs. So we have to refetch unassigned outputs + $unassignedInputs = $inputChannels.find('input[type="checkbox"]:not(:checked)'); + $unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked'); + } + else { + logger.debug("selecting 1 inputs") + $unassignedInputs.eq(0).iCheck('check').attr('checked', 'checked'); + } + } + + // ensure 2 outputs are selected + var $assignedOutputs = $outputChannels.find('input[type="checkbox"]:checked'); + var $unassignedOutputs = $outputChannels.find('input[type="checkbox"]:not(:checked)'); + + if ($assignedOutputs.length == 0) { + logger.debug("selecting both outputs") + $unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked'); + // this is required because iCheck change handler re-writes the inputs. So we have to refetch unassigned outputs + $unassignedOutputs = $outputChannels.find('input[type="checkbox"]:not(:checked)'); + $unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked'); + } + else if ($assignedOutputs.length == 1) { + logger.debug("selecting 1 output to round out 2 total") + $unassignedOutputs.eq(0).iCheck('check').attr('checked', 'checked'); + } + + return true; + } + + // during this phase of the FTUE, we have to assign selected input channels + // to tracks. The user, however, does not have a way to indicate which channel + // goes to which track (that's not until the next step of the wizard). + // so, we just auto-generate a valid assignment + function newInputAssignment() { + var assigned = 0; + context._.each(musicPorts.inputs, function (inputChannel) { + if (isChannelAssigned(inputChannel)) { + assigned += 1; + } + }); + + var newAssignment = Math.floor(assigned / 2) + 1; + return newAssignment; + } + + function inputChannelChanged() { + if (iCheckIgnore) return; + + var $checkbox = $(this); + var channelId = $checkbox.attr('data-id'); + var isChecked = $checkbox.is(':checked'); + + if (isChecked) { + var newAssignment = newInputAssignment(); + logger.debug("assigning input channel %o to track: %o", channelId, newAssignment); + context.jamClient.TrackSetAssignment(channelId, true, newAssignment); + } + else { + logger.debug("unassigning input channel %o", channelId); + context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.UNASSIGNED); + // unassigning creates a hole in our auto-assigned tracks. reassign them all to keep it consistent + var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked'); + var assigned = 0; + context._.each($assignedInputs, function (assignedInput) { + var $assignedInput = $(assignedInput); + var assignedChannelId = $assignedInput.attr('data-id'); + var newAssignment = Math.floor(assigned / 2) + 1; + logger.debug("re-assigning input channel %o to track: %o", assignedChannelId, newAssignment); + context.jamClient.TrackSetAssignment(assignedChannelId, true, newAssignment); + assigned += 1; + }); + } + + initializeChannels(); + } + + // should be called in a ifChanged callback if you want to cancel. + // you have to use this instead of 'return false' like a typical input 'change' event. + function cancelICheckChange($checkbox) { + iCheckIgnore = true; + var checked = $checkbox.is(':checked'); + setTimeout(function () { + if (checked) $checkbox.iCheck('uncheck').removeAttr('checked'); + else $checkbox.iCheck('check').attr('checked', 'checked'); + iCheckIgnore = false; + }, 1); + } + + function outputChannelChanged() { + if (iCheckIgnore) return; + var $checkbox = $(this); + var channelId = $checkbox.attr('data-id'); + var isChecked = $checkbox.is(':checked'); + + // don't allow more than 2 output channels selected at once + if ($outputChannels.find('input[type="checkbox"]:checked').length > 2) { + context.JK.Banner.showAlert('You can only have a maximum of 2 output ports selected.'); + // can't allow uncheck of last output + cancelICheckChange($checkbox); + return; + } + + if (isChecked) { + logger.debug("assigning output channel %o", channelId); + context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.OUTPUT); + } + else { + logger.debug("unassigning output channel %o", channelId); + context.jamClient.TrackSetAssignment(channelId, true, ASSIGNMENT.UNASSIGNED); + } + + initializeChannels(); + } + + // checks if it's an assigned OUTPUT or ASSIGNED CHAT + function isChannelAssigned(channel) { + return channel.assignment == ASSIGNMENT.CHAT || channel.assignment == ASSIGNMENT.OUTPUT || channel.assignment > 0; + } + + function initializeInputPorts(musicPorts) { + $inputChannels.empty(); + var inputPorts = musicPorts.inputs; + context._.each(inputPorts, function (inputChannel) { + var $inputChannel = $(context._.template($templateAudioPort.html(), inputChannel, { variable: 'data' })); + var $checkbox = $inputChannel.find('input'); + if (isChannelAssigned(inputChannel)) { + $checkbox.attr('checked', 'checked'); + } + context.JK.checkbox($checkbox); + $checkbox.on('ifChanged', inputChannelChanged); + $inputChannels.append($inputChannel); + }); + } + + function initializeOutputPorts(musicPorts) { + $outputChannels.empty(); + var outputChannels = musicPorts.outputs; + context._.each(outputChannels, function (outputChannel) { + var $outputPort = $(context._.template($templateAudioPort.html(), outputChannel, { variable: 'data' })); + var $checkbox = $outputPort.find('input'); + if (isChannelAssigned(outputChannel)) { + $checkbox.attr('checked', 'checked'); + } + context.JK.checkbox($checkbox); + $checkbox.on('ifChanged', outputChannelChanged); + $outputChannels.append($outputPort); + }); + } + + function initializeLoopback() { + $launchLoopbackBtn.unbind('click').click(function() { + app.setWizardStep(1); + app.layout.showDialog('ftue'); + return false; + }) + } + + function initializeFormElements() { + if (!deviceInformation) throw "devices are not initialized"; + + initializeAudioInput(); + initializeAudioOutput(); + initializeFramesize(); + initializeBuffers(); + initializeLoopback(); + } + + function resetFrameBuffers() { + $frameSize.val('2.5'); + $bufferIn.val('0'); + $bufferOut.val('0'); + } + + function clearInputPorts() { + $inputChannels.empty(); + } + + function clearOutputPorts() { + $outputChannels.empty(); + } + + function resetScoreReport() { + $ioHeader.hide(); + $latencyHeader.hide(); + $ioRate.hide(); + $ioRateScore.empty(); + $ioVar.hide(); + $ioVarScore.empty(); + $latencyScore.empty(); + $resultsText.removeAttr('latency-score'); + $resultsText.removeAttr('io-var-score'); + $resultsText.removeAttr('io-rate-score'); + $resultsText.removeAttr('scored'); + $unknownText.hide(); + $ioScoreSection.removeClass('good acceptable bad unknown starting skip'); + $latencyScoreSection.removeClass('good acceptable bad unknown starting') + } + + function renderLatencyScore(latencyValue, latencyClass) { + // latencyValue == null implies starting condition + if (latencyValue) { + $latencyScore.text(latencyValue + ' ms'); + } + else { + $latencyScore.text(''); + } + + + if(latencyClass == 'unknown') { + $latencyScore.text('Unknown'); + $unknownText.show(); + } + + $latencyHeader.show(); + $resultsText.attr('latency-score', latencyClass); + $latencyScoreSection.removeClass('good acceptable bad unknown starting').addClass(latencyClass); + } + + // std deviation is the worst value between in/out + // media is the worst value between in/out + // io is the value returned by the backend, which has more info + // ioClass is the pre-computed rollup class describing the result in simple terms of 'good', 'acceptable', bad' + function renderIOScore(std, median, ioData, ioClass, ioRateClass, ioVarClass) { + $ioRateScore.text(median !== null ? median : ''); + $ioVarScore.text(std !== null ? std : ''); + if (ioClass && ioClass != "starting" && ioClass != "skip") { + $ioRate.show(); + $ioVar.show(); + } + if(ioClass == 'starting' || ioClass == 'skip') { + $ioHeader.show(); + } + $resultsText.attr('io-rate-score', ioRateClass); + $resultsText.attr('io-var-score', ioVarClass); + $ioScoreSection.removeClass('good acceptable bad unknown starting skip') + if (ioClass) { + $ioScoreSection.addClass(ioClass); + } + // TODO: show help bubble of all data in IO data + } + + function updateScoreReport(latencyResult) { + var latencyClass = "neutral"; + var latencyValue = null; + var validLatency = false; + if (latencyResult && latencyResult.latencyknown) { + var latencyValue = latencyResult.latency; + latencyValue = Math.round(latencyValue * 100) / 100; + if (latencyValue <= 10) { + latencyClass = "good"; + validLatency = true; + } else if (latencyValue <= gon.ftue_maximum_gear_latency) { + latencyClass = "acceptable"; + validLatency = true; + } else { + latencyClass = "bad"; + } + } + else { + latencyClass = 'unknown'; + } + + validLatencyScore = validLatency; + + renderLatencyScore(latencyValue, latencyClass); + } + + function audioInputDeviceUnselected() { + validDevice = false; + setOutputAudioDevice(''); + resetState(); + } + + function renderScoringStarted() { + resetScoreReport(); + initializeNextButtonState(); + freezeAudioInteraction(); + renderLatencyScore(null, 'starting'); + renderIOScore(null, null, null, null, null, null); + } + + function renderScoringStopped() { + initializeNextButtonState(); + unfreezeAudioInteraction(); + $resultsText.attr('scored', 'complete'); + scoring = false; + initializeBackButtonState(); + } + + function freezeAudioInteraction() { + logger.debug("freezing audio interaction"); + $audioInput.attr("disabled", "disabled").easyDropDown('disable'); + $audioOutput.attr("disabled", "disabled").easyDropDown('disable'); + $frameSize.attr("disabled", "disabled").easyDropDown('disable'); + $bufferIn.attr("disabled", "disabled").easyDropDown('disable'); + $bufferOut.attr("disabled", "disabled").easyDropDown('disable'); + $asioInputControlBtn.on("click", false); + $asioOutputControlBtn.on("click", false); + $resyncBtn.on('click', false); + iCheckIgnore = true; + $inputChannels.find('input[type="checkbox"]').iCheck('disable'); + $outputChannels.find('input[type="checkbox"]').iCheck('disable'); + } + + function unfreezeAudioInteraction() { + logger.debug("unfreezing audio interaction"); + $audioInput.removeAttr("disabled").easyDropDown('enable'); + $audioOutput.removeAttr("disabled").easyDropDown('enable'); + $frameSize.removeAttr("disabled").easyDropDown('enable'); + $bufferIn.removeAttr("disabled").easyDropDown('enable'); + $bufferOut.removeAttr("disabled").easyDropDown('enable'); + $asioInputControlBtn.off("click", false); + $asioOutputControlBtn.off("click", false); + $resyncBtn.off('click', false); + $inputChannels.find('input[type="checkbox"]').iCheck('enable'); + $outputChannels.find('input[type="checkbox"]').iCheck('enable'); + iCheckIgnore = false; + } + + function initializeWatchVideo() { + $watchVideoInput.unbind('click').click(function () { + + var audioDevice = findDevice(selectedAudioInput()); + if (!audioDevice) { + context.JK.Banner.showAlert('You must first choose an Audio Input Device so that we can determine which video to show you.'); + } + else { + var videoURL = AUDIO_DEVICE_BEHAVIOR[audioDevice.type].videoURL; + + if (videoURL) { + $(this).attr('href', videoURL); + return true; + } + else { + context.JK.Banner.showAlert('No help video for this type of device (' + audioDevice.displayType + ')'); + } + } + + return false; + }); + + $watchVideoOutput.unbind('click').click(function () { + + var audioDevice = findDevice(selectedAudioOutput()); + if (!audioDevice) { + throw "this button should be hidden"; + } + else { + var videoURL = AUDIO_DEVICE_BEHAVIOR[audioDevice.type].videoURL; + + if (videoURL) { + $(this).attr('href', videoURL); + return true; + } + else { + context.JK.Banner.showAlert('No help video for this type of device (' + audioDevice.displayType + ')'); + } + } + + return false; + }); + } + + function invalidateScore() { + validLatencyScore = false; + validIOScore = false; + resetScoreReport(); + initializeNextButtonState(); + } + + function initializeASIOButtons() { + $asioInputControlBtn.unbind('click').click(function () { + context.jamClient.FTUEOpenControlPanel(selectedAudioInput()); + }); + $asioOutputControlBtn.unbind('click').click(function () { + context.jamClient.FTUEOpenControlPanel(selectedAudioOutput()); + }); + } + + function initializeKnobs() { + $frameSize.unbind('change').change(function () { + logger.debug("frameize changed: " + selectedFramesize()); + context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']}); + updateDefaultBuffers(); + jamClient.FTUESetFrameSize(selectedFramesize()); + invalidateScore(); + }); + + $bufferIn.unbind('change').change(function () { + logger.debug("buffer-in changed: " + selectedBufferIn()); + context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']}); + jamClient.FTUESetInputLatency(selectedBufferIn()); + invalidateScore(); + }); + + $bufferOut.unbind('change').change(function () { + logger.debug("buffer-out changed: " + selectedBufferOut()); + context.JK.prodBubble($resyncBtn, 'push-resync-when-done', {}, {positions:['top']}); + jamClient.FTUESetOutputLatency(selectedBufferOut()); + invalidateScore(); + }); + } + + function ftueSummary() { + return { + os: operatingSystem, + version: context.jamClient.ClientUpdateVersion(), + success: isGoodFtue(), + score: { + validLatencyScore: validLatencyScore, + validIOScore: validIOScore, + latencyScore: latencyScore, + ioScore : ioScore, + }, + audioParameters: { + frameSize: selectedFramesize(), + bufferIn: selectedBufferIn(), + bufferOut: selectedBufferOut(), + }, + devices: deviceInformation, + selectedDevice: selectedDeviceInfo + } + } + + function postDiagnostic() { + rest.createDiagnostic({ + type: 'GEAR_SELECTION', + data: { + logs: logger.logCache, + client_type: context.JK.clientType(), + client_id: + context.JK.JamServer.clientID, + summary:ftueSummary()} + }); + } + + function getSelectedInputs() { + return $inputChannels.find('input[type="checkbox"]:checked'); + } + + function getSelectedOutputs() { + return $outputChannels.find('input[type="checkbox"]:checked'); + } + + function initializeResync() { + $resyncBtn.unbind('click').click(function () { + + if (getSelectedInputs().length == 0) { + context.JK.Banner.showAlert("You must have at least one input port to resync."); + return false; + } + + if (getSelectedOutputs().length < 2) { + context.JK.Banner.showAlert("You must have exactly two output ports to resync."); + return false; + } + + attemptScore(); + return false; + }) + } + + function renderIOScoringStarted(secondsLeft) { + $ioCountdownSecs.text(secondsLeft); + $ioCountdown.show(); + } + + function renderIOScoringStopped() { + $ioCountdown.hide(); + } + + function renderIOCountdown(secondsLeft) { + $ioCountdownSecs.text(secondsLeft); + } + + // sets selectedDeviceInfo, which contains id, behavior, and info for input and output device + function cacheCurrentAudioInfo() { + + var audioInputDeviceId = selectedAudioInput(); + var audioOutputDeviceId = selectedAudioOutput(); + if (!audioInputDeviceId) { + audioInputDeviceUnselected(); + return false; + } + + var audioInputDevice = findDevice(audioInputDeviceId); + if (!audioInputDevice) { + context.JK.alertSupportedNeeded('Unable to find information for input device: ' + audioInputDeviceId); + return false; + } + + if (!audioOutputDeviceId) { + audioOutputDeviceId = audioInputDeviceId; + } + var audioOutputDevice = findDevice(audioOutputDeviceId); + if (!audioInputDevice) { + context.JK.alertSupportedNeeded('Unable to find information for output device: ' + audioOutputDeviceId); + return false; + } + + if(savedProfile && jamClient.FTUEGetInputMusicDevice() != audioInputDeviceId || jamClient.FTUEGetOutputMusicDevice() != audioOutputDeviceId) { + // if they want to change an input or output device, but we have a saved (named) profile + // then we need to delete the current profile and create a new one + // because there is no way to rename a profile, and the profile name has the device's name in it + var profileName = context.jamClient.FTUEGetMusicProfileName(); + logger.debug("invaliding previously saved profile: " + profileName); + + $dialog.createFTUEProfile(); + // restore user selections because newSession is called by createFTUEProfile(), invalidating dropdowns + setInputAudioDevice(audioInputDeviceId); + setOutputAudioDevice(audioOutputDeviceId); + + context.jamClient.TrackDeleteProfile(profileName); + + } + + shownOutputProdOnce = false; + shownInputProdOnce = false; + + lastSelectedDeviceInfo = selectedDeviceInfo; + + selectedDeviceInfo = context.JK.selectedDeviceInfo(audioInputDeviceId, audioOutputDeviceId, deviceInformation) + + return true; + } + + function changeDevice() { + + var audioInputDeviceId = selectedDeviceInfo.input.id; + var audioOutputDeviceId = selectedDeviceInfo.output.id; + + if(audioInputDeviceId) { + $audioOutput.easyDropDown('enable'); + } + + // don't re-assign input/output audio devices because it disturbs input/output track association + if (jamClient.FTUEGetInputMusicDevice() != audioInputDeviceId) { + jamClient.FTUESetInputMusicDevice(audioInputDeviceId); + } + if (jamClient.FTUEGetOutputMusicDevice() != audioOutputDeviceId) { + jamClient.FTUESetOutputMusicDevice(audioOutputDeviceId); + } + + initializeChannels(); + + validDevice = autoSelectMinimumValidChannels(); + + if (!validDevice) { + return false; + } + + jamClient.FTUESetInputLatency(selectedBufferIn()); + jamClient.FTUESetOutputLatency(selectedBufferOut()); + jamClient.FTUESetFrameSize(selectedFramesize()); + + return true; + } + + function audioDeviceChanged() { + if(!cacheCurrentAudioInfo()) { + return; + } + updateDialogForCurrentDevices(); + if (changeDevice()) { + attemptScore(); + } + } + + function isInputAudioTypeDifferentFromLastTime() { + return lastSelectedDeviceInfo && (lastSelectedDeviceInfo.input.info.type != selectedDeviceInfo.input.info.type); + } + + function isOutputAudioTypeDifferentFromLastTime() { + return lastSelectedDeviceInfo && isInputOutputDifferentTypes() && (lastSelectedDeviceInfo.output.info.type != selectedDeviceInfo.output.info.type) + } + + function isInputOutputDifferentTypes() { + return selectedDeviceInfo && (selectedDeviceInfo.input.info.type != selectedDeviceInfo.output.info.type); + } + + function updateDialogForCurrentDevices() { + if(selectedDeviceInfo) { + var inputBehavior = selectedDeviceInfo.input.behavior; + var outputBehavior = selectedDeviceInfo.output.behavior; + } + else { + var inputBehavior = null; + var outputBehavior = null; + + } + + // deal with watch video + if(isInputOutputDifferentTypes()) { + // if we have two types of devices, you need two different videos + $watchVideoOutput.show().find('.video-type').show(); + $watchVideoInput.addClass('audio-output-showing').find('.video-type').show(); + } + else { + $watchVideoOutput.hide(); + $watchVideoInput.removeClass('audio-output-showing').find('.video-type').hide(); + } + + // handle framesize/buffers + if (inputBehavior && (inputBehavior.showKnobs || outputBehavior.showKnobs)) { + $knobs.show(); + } + else { + $knobs.hide(); + } + + // handle ASIO + if (inputBehavior) { + if (inputBehavior.showASIO) { + $asioInputControlBtn.show(); + } + else { + $asioInputControlBtn.hide(); + } + if(outputBehavior.showASIO && (selectedDeviceInfo.input.id != selectedDeviceInfo.output.id)) { + $asioOutputControlBtn.show(); + } + else { + $asioOutputControlBtn.hide(); + } + } + else { + // show no ASIO buttons + $asioInputControlBtn.hide(); + $asioOutputControlBtn.hide(); + } + + // handle resync button + if (inputBehavior) { + $resyncBtn.css('visibility', 'visible'); + } + else { + $resyncBtn.css('visibility', 'hidden'); + } + + updateDefaultFrameSize(); + + updateDefaultBuffers(); + } + + function updateDefaultFrameSize() { + if(selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm')) { + setFramesize('10'); + } + else { + setFramesize('2.5') + } + + jamClient.FTUESetFrameSize(selectedFramesize()); + } + + function updateDefaultBuffers() { + + // handle specific framesize settings + if(selectedDeviceInfo && (selectedDeviceInfo.input.info.type == 'Win32_wdm' || selectedDeviceInfo.output.info.type == 'Win32_wdm')) { + var framesize = selectedFramesize(); + + if(framesize == 2.5) { + logger.debug("setting default buffers to 1/1"); + setBufferIn('1'); + setBufferOut('1'); + } + else if(framesize == 5) { + logger.debug("setting default buffers to 3/2"); + setBufferIn('3'); + setBufferOut('2'); + } + else { + logger.debug("setting default buffers to 6/5"); + setBufferIn('6'); + setBufferOut('5'); + } + } + else { + logger.debug("setting default buffers to 0/0"); + setBufferIn(0); + setBufferOut(0); + } + + jamClient.FTUESetInputLatency(selectedBufferIn()); + jamClient.FTUESetOutputLatency(selectedBufferOut()); + } + + function processIOScore(io) { + // take the higher variance, which is apparently actually std dev + var std = io.in_var > io.out_var ? io.in_var : io.out_var; + std = Math.round(std * 100) / 100; + // take the furthest-off-from-target io rate + var median = Math.abs(io.in_median - io.in_target) > Math.abs(io.out_median - io.out_target) ? [io.in_median, io.in_target] : [io.out_median, io.out_target]; + var medianTarget = median[1]; + median = Math.round(median[0]); + + var stdIOClass = 'bad'; + if (std <= 0.50) { + stdIOClass = 'good'; + } + else if (std <= 1.00) { + stdIOClass = 'acceptable'; + } + + var medianIOClass = 'bad'; + if (Math.abs(median - medianTarget) <= 1) { + medianIOClass = 'good'; + } + else if (Math.abs(median - medianTarget) <= 2) { + medianIOClass = 'acceptable'; + } + + // take worst between median or std + var ioClassToNumber = {bad: 2, acceptable: 1, good: 0} + var aggregrateIOClass = ioClassToNumber[stdIOClass] > ioClassToNumber[medianIOClass] ? stdIOClass : medianIOClass; + + // now base the overall IO score based on both values. + renderIOScore(std, median, io, aggregrateIOClass, medianIOClass, stdIOClass); + + if(aggregrateIOClass == "bad") { + validIOScore = false; + } + else { + validIOScore = true; + } + + if(isGoodFtue()) { + onSuccessfulScore(); + } + else { + onFailedScore(); + } + + renderScoringStopped(); + postDiagnostic(); + } + + function onFailedScore() { + rest.userCertifiedGear({success: false}); + } + + function onSuccessfulScore() { + rest.userCertifiedGear({success: true}); + } + + // refocused affects how IO testing occurs. + // on refocus=true: + // * reuse IO score if it was good/acceptable + // * rescore IO if it was bad or skipped from previous try + function attemptScore(refocused) { + if(scoring) {return;} + scoring = true; + initializeBackButtonState(); + validLatencyScore = false; + latencyScore = null; + if(!refocused) { + // don't reset a valid IO score on refocus + validIOScore = false; + ioScore = null; + } + + renderScoringStarted(); + + // this timer exists to give UI time to update for renderScoringStarted before blocking nature of jamClient.FTUESave(save) kicks in + setTimeout(function () { + logger.debug("Calling FTUESave(false)"); + jamClient.FTUESave(false); + + var latency = jamClient.FTUEGetExpectedLatency(); + latencyScore = latency; + + // prod user to watch video if the previous type and new type changed + if(!shownInputProdOnce && isInputAudioTypeDifferentFromLastTime()) { + context.JK.prodBubble($watchVideoInput, 'ftue-watch-video', {}, {positions:['top', 'right']}); + shownInputProdOnce = true; + } + + // prod user to watch video if the previous type and new type changed + if(!shownOutputProdOnce && isOutputAudioTypeDifferentFromLastTime()) { + context.JK.prodBubble($watchVideoOutput, 'ftue-watch-video', {}, {positions:['top', 'right']}); + shownOutputProdOnce = true; + } + + updateScoreReport(latency); + + if(refocused) { + context.JK.prodBubble($scoreReport, 'refocus-rescore', {validIOScore: validIOScore}, {positions:['top', 'left']}); + } + + // if there was a valid latency score, go on to the next step + if (validLatencyScore) { + // reuse valid IO score if this is on refocus + if(refocused && validIOScore) { + renderIOScore(null, null, null, 'starting', 'starting', 'starting'); + processIOScore(ioScore); + } + else { + renderIOScore(null, null, null, 'starting', 'starting', 'starting'); + var testTimeSeconds = gon.ftue_io_wait_time; // allow time for IO to establish itself + var startTime = testTimeSeconds / 2; // start measuring half way through the test, to get past IO oddities + renderIOScoringStarted(testTimeSeconds); + renderIOCountdown(testTimeSeconds); + var interval = setInterval(function () { + testTimeSeconds -= 1; + renderIOCountdown(testTimeSeconds); + + if(testTimeSeconds == startTime) { + logger.debug("Starting IO Perf Test starting at " + startTime + "s in") + context.jamClient.FTUEStartIoPerfTest(); + } + + if (testTimeSeconds == 0) { + clearInterval(interval); + renderIOScoringStopped(); + logger.debug("Ending IO Perf Test at " + testTimeSeconds + "s in") + var io = context.jamClient.FTUEGetIoPerfData(); + ioScore = io; + processIOScore(io); + } + }, 1000); + } + } + else { + onFailedScore(); + renderIOScore(null, null, null, 'skip', 'skip', 'skip'); + renderScoringStopped(); + postDiagnostic(); + } + }, 250); + } + + function initializeAudioInputChanged() { + $audioInput.unbind('change').change(audioDeviceChanged); + } + + function initializeAudioOutputChanged() { + $audioOutput.unbind('change').change(audioDeviceChanged); + } + + function handleNext() { + + if(!savedProfile) { + var $assignedInputs = $inputChannels.find('input[type="checkbox"]:checked'); + var $assignedOutputs = $outputChannels.find('input[type="checkbox"]:checked'); + + var errors = []; + if($assignedInputs.length == 0) { + errors.push("There must be at least one selected input ports."); + } + if($assignedInputs.length > 12) { + errors.push("There can only be up to 12 selected inputs ports."); + } + if($assignedOutputs.length != 2) { + errors.push("There must be exactly 2 selected output ports."); + } + var $errors = $('
    '); + context._.each(errors, function(error) { + $errors.append('
  • ' + error + '
  • '); + }); + + if(errors.length > 0) { + context.JK.Banner.showAlert({html:$errors}); + return false; + } + else { + context.jamClient.FTUESetMusicProfileName(context.JK.createProfileName(selectedDeviceInfo)); + context.jamClient.FTUESave(true); + savedProfile = true; + context.JK.GA.trackAudioTestCompletion(context.JK.detectOS()); + return true; + } + } + } + + function onFocus() { + if(!scoring && validDevice && getSelectedInputs().length > 0 && getSelectedOutputs().length == 2 ) { + // in the case the user has been unselecting ports, re-enforce minimum viable channels + validDevice = autoSelectMinimumValidChannels(); + attemptScore(true); + } + } + + function newSession() { + savedProfile = false; + deviceInformation = context.JK.loadDeviceInfo(); + resetState(); + initializeFormElements(); + initializeNextButtonState(); + initializeWatchVideo(); + initializeASIOButtons(); + initializeKnobs(); + initializeResync(); + } + + function beforeShow() { + $(window).on('focus', onFocus); + } + + function beforeHide() { + logger.debug("unregistering focus watch") + $(window).off('focus', onFocus); + } + + function resetState() { + selectedDeviceInfo = null; + invalidateScore(); + clearInputPorts(); + clearOutputPorts(); + resetFrameBuffers(); + updateDialogForCurrentDevices(); + } + + function initialize(_$step) { + $step = _$step; + + $watchVideoInput = $step.find('.watch-video.audio-input'); + $watchVideoOutput = $step.find('.watch-video.audio-output'); + $audioInput = $step.find('.select-audio-input-device'); + $audioOutput = $step.find('.select-audio-output-device'); + $bufferIn = $step.find('.select-buffer-in'); + $bufferOut = $step.find('.select-buffer-out'); + $frameSize = $step.find('.select-frame-size'); + $inputChannels = $step.find('.input-ports'); + $outputChannels = $step.find('.output-ports'); + $knobs = $step.find('.frame-and-buffers'); + $scoreReport = $step.find('.results'); + $latencyScoreSection = $scoreReport.find('.latency-score-section'); + $latencyScore = $scoreReport.find('.latency-score'); + $latencyHeader = $scoreReport.find('.latency'); + $ioHeader = $scoreReport.find('.io'); + $ioScoreSection = $scoreReport.find('.io-score-section'); + $ioRate = $scoreReport.find('.io-rate'); + $ioRateScore = $scoreReport.find('.io-rate-score'); + $ioVar = $scoreReport.find('.io-var'); + $ioVarScore = $scoreReport.find('.io-var-score'); + $ioCountdown = $scoreReport.find('.io-countdown'); + $ioCountdownSecs = $scoreReport.find('.io-countdown .secs'); + $resultsText = $scoreReport.find('.results-text'); + $unknownText = $scoreReport.find('.unknown-text'); + $asioInputControlBtn = $step.find('.asio-settings-input-btn'); + $asioOutputControlBtn = $step.find('.asio-settings-output-btn'); + $resyncBtn = $step.find('.resync-btn'); + $templateAudioPort = $('#template-audio-port'); + $launchLoopbackBtn = $('.loopback-test'); + $instructions = $('.instructions'); + operatingSystem = context.JK.GetOSAsString(); + } + + this.handleNext = handleNext; + this.newSession = newSession; + this.beforeShow = beforeShow; + this.beforeHide = beforeHide; + this.initialize = initialize; + + self = this; + return this; + }; + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/wizard/gear/step_success.js b/web/app/assets/javascripts/wizard/gear/step_success.js new file mode 100644 index 000000000..5a1f03fc4 --- /dev/null +++ b/web/app/assets/javascripts/wizard/gear/step_success.js @@ -0,0 +1,23 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepSuccess = function (app, $dialog) { + + var $step = null; + + function beforeShow() { + + } + + function initialize(_$step) { + $step = _$step; + } + + this.beforeShow = beforeShow; + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/wizard/gear/step_understand_gear.js b/web/app/assets/javascripts/wizard/gear/step_understand_gear.js new file mode 100644 index 000000000..c4c235cf1 --- /dev/null +++ b/web/app/assets/javascripts/wizard/gear/step_understand_gear.js @@ -0,0 +1,31 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.StepUnderstandGear = function (app) { + + var $step = null; + var operatingSystem; + + function beforeShow() { + var $watchVideo = $step.find('.watch-video'); + var videoUrl = 'https://www.youtube.com/watch?v=VexH4834o9I'; + if (operatingSystem == "Win32") { + $watchVideo.attr('href', 'https://www.youtube.com/watch?v=VexH4834o9I'); + } + $watchVideo.attr('href', videoUrl); + } + + function initialize(_$step) { + $step = _$step; + + operatingSystem = context.JK.GetOSAsString(); + } + + this.beforeShow = beforeShow; + this.initialize = initialize; + + return this; + } +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/wizard/loopback/loopback_wizard.js b/web/app/assets/javascripts/wizard/loopback/loopback_wizard.js new file mode 100644 index 000000000..6202cb76f --- /dev/null +++ b/web/app/assets/javascripts/wizard/loopback/loopback_wizard.js @@ -0,0 +1,97 @@ +(function (context, $) { + + "use strict"; + + + context.JK = context.JK || {}; + context.JK.LoopbackWizard = function (app) { + + var logger = context.JK.logger; + + var $dialog = null; + var wizard = null; + var $wizardSteps = null; + + var step1 = new context.JK.Step1(app, this); + var step2 = new context.JK.Step2(app, this); + var step3 = new context.JK.Step3(app, this); + + var STEPS = { + 0: step1, + 1: step2, + 2: step3 + } + + function beforeShow(args) { + wizard.onBeforeShow(args); + } + + function afterHide() { + wizard.onAfterHide(); + } + + function closeDialog() { + wizard.onCloseDialog(); + app.layout.closeDialog('your-wizard'); + } + + function setNextState(enabled) { + wizard.setNextState(enabled); + } + + function setBackState(enabled) { + wizard.setBackState(enabled); + } + + + function onStepChanged(e, data) { + var step = wizard.getCurrentStep(); + var $currentWizardStep = wizard.getCurrentWizardStep(); + } + + + function onCanceled() { + closeDialog(); + return false; + } + + function onClosed() { + closeDialog(); + return false; + } + + + function events() { + $(wizard).on('step_changed', onStepChanged); + $(wizard).on('wizard_cancel', onCanceled); + $(wizard).on('wizard_close', onClosed); + } + + + function initialize() { + + + var dialogBindings = { beforeShow: beforeShow, afterHide: afterHide }; + + app.bindDialog('your-wizard', dialogBindings); + $dialog = $('#your-wizard'); + $wizardSteps = $dialog.find('.wizard-step'); + + step1.initialize($wizardSteps.filter($('[layout-wizard-step=0]'))); + step2.initialize($wizardSteps.filter($('[layout-wizard-step=1]'))); + step3.initialize($wizardSteps.filter($('[layout-wizard-step=2]'))); + + wizard = new context.JK.Wizard(app); + wizard.initialize($dialog, $wizardSteps, STEPS); + + events(); + } + + this.setNextState = setNextState; + this.setBackState = setBackState; + this.initialize = initialize; + + return this; + + } +})(window, jQuery); diff --git a/web/app/assets/javascripts/wizard/wizard.js b/web/app/assets/javascripts/wizard/wizard.js new file mode 100644 index 000000000..88e53a630 --- /dev/null +++ b/web/app/assets/javascripts/wizard/wizard.js @@ -0,0 +1,215 @@ +(function (context, $) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.Wizard = function (app) { + + var STEPS = null; + var step = null; + var previousStep = null; + var $dialog = null; + var $templateButtons = null; + var $wizardSteps = null; + var $currentWizardStep = null; + var $wizardButtons = null; + var $btnHelp = null; + var $btnNext = null; + var $btnBack = null; + var $btnClose = null; + var $btnCancel = null; + var self = this; + var $self = $(this); + + function totalSteps() { + return context.JK.dkeys(STEPS).length; + } + + function beforeHideStep() { + var currentStep = getCurrentStep(); + if(currentStep === null) return; + + var stepInfo = STEPS[currentStep]; + + if (!stepInfo) { + throw "unknown step: " + currentStep; + } + + if(stepInfo.beforeHide) { + stepInfo.beforeHide.call(stepInfo); + } + } + + function beforeShowStep($step) { + var stepInfo = STEPS[step]; + + if (!stepInfo) { + throw "unknown step: " + step; + } + + stepInfo.beforeShow.call(stepInfo); + } + + function back() { + if ($(this).is('.button-grey')) return false; + previousStep = step; + step = step - 1; + moveToStep(); + return false; + } + + function next() { + if ($(this).is('.button-grey')) return false; + + var stepInfo = STEPS[step]; + if(stepInfo.handleNext) { + var result = stepInfo.handleNext.call(stepInfo); + if(result === false) {return false;} + } + + previousStep = step; + step = step + 1; + + moveToStep(); + return false; + } + + function moveToStep() { + var $nextWizardStep = $wizardSteps.filter($('[layout-wizard-step=' + step + ']')); + + beforeHideStep(); + + $wizardSteps.hide(); + + $currentWizardStep = $nextWizardStep; + + context.JK.GA.virtualPageView(location.pathname + location.search + location.hash + '/d1=' + step, $currentWizardStep.attr('dialog-title')); + + $self.triggerHandler('step_changed', {step:step}); + + // update buttons + var $wizardButtonsContent = $(context._.template($templateButtons.html(), {}, {variable: 'data'})); + + $btnHelp = $wizardButtonsContent.find('.btn-help'); + $btnBack = $wizardButtonsContent.find('.btn-back'); + $btnNext = $wizardButtonsContent.find('.btn-next'); + $btnClose = $wizardButtonsContent.find('.btn-close'); + $btnCancel = $wizardButtonsContent.find('.btn-cancel'); + + // hide help button if on last step + if (step == totalSteps() - 1) { + $btnHelp.hide(); + } + // hide back button if 1st step or last step + if (step == 0 || step == totalSteps() - 1) { + $btnBack.hide(); + } + // hide next button if on last step + if (step == totalSteps() - 1) { + $btnNext.hide(); + } + // hide close if not on last step + if (step != totalSteps() - 1) { + $btnClose.hide(); + } + // hide cancel if not on last step + if (step == totalSteps() - 1) { + $btnCancel.hide(); + } + + $btnNext.on('click', next); + $btnBack.on('click', back); + $btnClose.on('click', function() {$self.triggerHandler('wizard_close'); return false;}); + $btnCancel.on('click', function() {$self.triggerHandler('wizard_cancel'); return false;}); + + $wizardButtons.empty(); + $wizardButtons.append($wizardButtonsContent); + + beforeShowStep($currentWizardStep); + $currentWizardStep.show(); + + } + + // called by owner whenever + function onCloseDialog() { + beforeHideStep(); + } + + function onBeforeShow(args) { + + $currentWizardStep = null; + previousStep = null; + + step = args != null ? args.d1 : 0; + if (!step) step = 0; + step = parseInt(step); + moveToStep(); + } + + function onAfterHide() { + step = null; + } + + function setNextState(enabled) { + + if(!$btnNext) return; + + $btnNext.removeClass('button-orange button-grey'); + + if (enabled) { + $btnNext.addClass('button-orange'); + } + else { + $btnNext.addClass('button-grey'); + } + } + + function setBackState(enabled) { + + if(!$btnBack) return; + + $btnBack.removeClass('button-orange button-grey'); + + if (enabled) { + $btnBack.addClass('button-orange'); + } + else { + $btnBack.addClass('button-grey'); + } + } + + function getCurrentStep() { + if($currentWizardStep) { + return parseInt($currentWizardStep.attr('layout-wizard-step')); + } + else { + return null; + } + + } + + function getCurrentWizardStep() { + return $currentWizardStep; + } + + function initialize(_$dialog, _$wizardSteps, _STEPS) { + $dialog = _$dialog; + $wizardSteps = _$wizardSteps; + STEPS = _STEPS; + + $wizardButtons = $dialog.find('.wizard-buttons'); + $templateButtons = $('#template-wizard-buttons'); + } + + this.setNextState = setNextState; + this.setBackState = setBackState; + this.getCurrentStep = getCurrentStep; + this.getCurrentWizardStep = getCurrentWizardStep; + this.onCloseDialog = onCloseDialog; + this.onBeforeShow = onBeforeShow; + this.onAfterHide = onAfterHide; + this.initialize = initialize; + + } + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/account.css.scss b/web/app/assets/stylesheets/client/account.css.scss index 35beafe14..a1adfda4a 100644 --- a/web/app/assets/stylesheets/client/account.css.scss +++ b/web/app/assets/stylesheets/client/account.css.scss @@ -237,6 +237,14 @@ .actions { text-align: center; } + + td span[data-current="true"] { + font-size:11px; + } + + tr[data-status="bad"] { + color:grey; + } } } diff --git a/web/app/assets/stylesheets/client/banner.css.scss b/web/app/assets/stylesheets/client/banner.css.scss index 4db4a318d..897d09f0e 100644 --- a/web/app/assets/stylesheets/client/banner.css.scss +++ b/web/app/assets/stylesheets/client/banner.css.scss @@ -26,5 +26,26 @@ .close-btn { display:none; } + + ul { + list-style:disc; + margin-left:20px; + } + li { + margin: 15px 12px 15px 36px; + } + + .end-content { + height: 0; + line-height: 0; + display: block; + font-size: 0; + content: " "; + } + + p { + line-height:20px; + margin-bottom:10px; + } } diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css index 4b34dc6f0..3c3e2a6fb 100644 --- a/web/app/assets/stylesheets/client/client.css +++ b/web/app/assets/stylesheets/client/client.css @@ -34,7 +34,8 @@ *= require ./search *= require ./ftue *= require ./jamServer - *= require ./gearWizard + *= require ./wizard/wizard + *= require ./wizard/gearWizard *= require ./whatsNextDialog *= require ./invitationDialog *= require ./shareDialog @@ -46,6 +47,7 @@ *= require ./textMessageDialog *= require ./acceptFriendRequestDialog *= require ./launchAppDialog + *= require ./iconInstrumentSelect *= require ./terms *= require ./createSession *= require ./feed diff --git a/web/app/assets/stylesheets/client/content.css.scss b/web/app/assets/stylesheets/client/content.css.scss index ecfef5fe6..79d7b1be6 100644 --- a/web/app/assets/stylesheets/client/content.css.scss +++ b/web/app/assets/stylesheets/client/content.css.scss @@ -337,7 +337,7 @@ a.arrow-down { .settings-session-description { padding:10px; - width:300px; + width:200px; } #session-controls { diff --git a/web/app/assets/stylesheets/client/ftue.css.scss b/web/app/assets/stylesheets/client/ftue.css.scss index a5f6796de..88e1230fd 100644 --- a/web/app/assets/stylesheets/client/ftue.css.scss +++ b/web/app/assets/stylesheets/client/ftue.css.scss @@ -83,15 +83,14 @@ div.dialog.ftue .ftue-inner div[layout-wizard-step="0"] { font-size: 0.9em; } + div.dialog.ftue { - min-width: 800px; - max-width: 800px; - min-height: 400px; - max-height: 400px; + min-height: 500px; + max-height: 500px; + width: 800px; .ftue-inner { line-height: 1.3em; - width: auto; a { text-decoration: underline; diff --git a/web/app/assets/stylesheets/client/iconInstrumentSelect.css.scss b/web/app/assets/stylesheets/client/iconInstrumentSelect.css.scss new file mode 100644 index 000000000..601222f00 --- /dev/null +++ b/web/app/assets/stylesheets/client/iconInstrumentSelect.css.scss @@ -0,0 +1,47 @@ +.icon-instrument-select { + .current-instrument { + width:24px; + height:24px; + font-size:20px; + cursor:pointer; + + &.none { + height:22px; + width:22px; + border:1px solid white; + -webkit-border-radius:12px; + -moz-border-radius:12px; + border-radius:12px; + line-height:22px; + vertical-align: middle; + text-align:center; + } + } + ul { + display:none; + border:1px solid white; + max-height:250px; + position:absolute; + overflow:auto; + width:150px; + background-color:#333; + text-align:left; + } +} + +.icon-instrument-selector-popup { + .bt-content { + height:150px; + width:150px; + background-color:#333; + text-align:left; + overflow:auto; + border:1px solid #ED3618; + + li { + margin-left:5px; + list-style-type: none; + } + } + +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/client/jamServer.css.scss b/web/app/assets/stylesheets/client/jamServer.css.scss index 95e1ecf34..44955e99f 100644 --- a/web/app/assets/stylesheets/client/jamServer.css.scss +++ b/web/app/assets/stylesheets/client/jamServer.css.scss @@ -3,6 +3,10 @@ text-align:center; width:100%; position:absolute; + + &.active { + display:block; + } } .server-connection { diff --git a/web/app/assets/stylesheets/client/jamkazam.css.scss b/web/app/assets/stylesheets/client/jamkazam.css.scss index 92d551f31..1610a3345 100644 --- a/web/app/assets/stylesheets/client/jamkazam.css.scss +++ b/web/app/assets/stylesheets/client/jamkazam.css.scss @@ -383,7 +383,7 @@ input[type="text"], input[type="password"]{ } .error input { - background-color:#Fadfd1 !important; + background-color:#fadfd1 !important; margin-bottom:5px; } diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss index 387352b0d..302d345b7 100644 --- a/web/app/assets/stylesheets/client/session.css.scss +++ b/web/app/assets/stylesheets/client/session.css.scss @@ -15,6 +15,31 @@ overflow-x:hidden; position:relative; } + + + .track { + width:70px; + height:290px; + display:inline-block; + margin-right:8px; + position:relative; + background-color:#242323; + + .disabled-track-overlay { + width:100%; + height:100%; + position:absolute; + background-color:#555; + opacity:0.5; + display:none; + } + } + + .track-instrument { + position:absolute; + top:85px; + left:12px; + } } @@ -25,16 +50,6 @@ color:#666; } - -.track { - width:70px; - height:290px; - display:inline-block; - margin-right:8px; - position:relative; - background-color:#242323; -} - .track-empty { min-width:230px; height:201px; @@ -268,23 +283,6 @@ table.vu td { } -.track { - width:70px; - height:290px; - display:inline-block; - margin-right:8px; - position:relative; - background-color:#242323; - - .disabled-track-overlay { - width:100%; - height:100%; - position:absolute; - background-color:#555; - opacity:0.5; - display:none; - } -} .track-empty { @@ -412,11 +410,6 @@ table.vu td { border-radius:22px; } -.track-instrument { - position:absolute; - top:85px; - left:12px; -} .track-gain { position:absolute; diff --git a/web/app/assets/stylesheets/client/sidebar.css.scss b/web/app/assets/stylesheets/client/sidebar.css.scss index 9491f54c0..ff21873e4 100644 --- a/web/app/assets/stylesheets/client/sidebar.css.scss +++ b/web/app/assets/stylesheets/client/sidebar.css.scss @@ -206,7 +206,7 @@ .chat-message-sender { font-weight:bold; margin-right:10px; - color: #020C81; + color: #ED3618; &:after { content:':' diff --git a/web/app/assets/stylesheets/client/gearWizard.css.scss b/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss similarity index 51% rename from web/app/assets/stylesheets/client/gearWizard.css.scss rename to web/app/assets/stylesheets/client/wizard/gearWizard.css.scss index aa2ac4bf6..9264961d2 100644 --- a/web/app/assets/stylesheets/client/gearWizard.css.scss +++ b/web/app/assets/stylesheets/client/wizard/gearWizard.css.scss @@ -34,6 +34,7 @@ margin-right: 10px; font-family: helvetica; text-decoration: none; + line-height: 27px; &.active { background-color:#ed3618; @@ -57,52 +58,12 @@ margin-top:20px; } - .ftue-buttons { - position: absolute; - bottom: 0; - width:100%; - @include border_box_sizing; - padding-right:45px; - padding-bottom:15px; - - } - - .ftue-buttons-holder { - float:right; - } - - .wizard-step { - display:none; - position:relative; - } - - .wizard-step-content { - margin-top:20px; - padding-bottom:12px; - - .wizard-step-column{ - position:relative; - float:left; - vertical-align:top; - @include border_box_sizing; - padding-right:12px; - height:300px; - } - - .wizard-step-column:last-child { - padding-right:0; - } - - p:nth-of-type(1) { - margin-top:0; - } - } - h2 { color: #FFFFFF; font-size: 15px; font-weight: normal; margin-bottom: 6px; + white-space:nowrap; } .ftue-box { background-color: #222222; @@ -115,10 +76,18 @@ &.list.ports { height:100px; + overflow:auto; } &.instructions { - height: 230px !important + height: 268px !important; + line-height:16px; + @include border_box_sizing; + + .video-type { + font-size:10px; + display:none; + } } ul li { @@ -127,7 +96,7 @@ } .watch-video { - margin:8px 0 15px 0; + margin:8px 0 3px 0; } } @@ -143,6 +112,18 @@ width:25%; } + .watch-video.audio-input { + margin-top:29px; + + &.audio-output-showing { + margin-top:10px; + } + } + + .watch-video.audio-output { + margin-top:10px; + } + .select-audio-input-device { margin-bottom:20px; } @@ -151,18 +132,32 @@ margin-bottom:20px; } - .asio-settings-btn, .resync-btn { + .asio-settings-input-btn, .asio-settings-output-btn, .resync-btn { width:80%; display:inline-block; text-align:center; } - .asio-settings-btn { + .asio-settings-input-btn, .asio-settings-output-btn { margin-top:10px; } + .asio-settings-input-btn { + display:none; + } + + .asio-settings-output-btn { + display:none; + } + .resync-btn { margin-top:10px; + visibility:hidden; + } + + .frame-and-buffers { + display:none; + margin-top:5px; } .framesize { @@ -171,11 +166,6 @@ width:30%; } - .buffers { - float:left; - width:60%; - } - .select-buffer-in { width:45%; } @@ -186,6 +176,16 @@ .buffers { + float:left; + width:60%; + + h2 { + margin-left:5px; + } + + .dropdown-container { + width:48px; + } .easydropdown-wrapper:nth-of-type(1) { left:5px; } @@ -193,15 +193,41 @@ left:35px; } .easydropdown, .easydropdown-wrapper { - width:15px; + width:30px; } } + .audio-port { + white-space: nowrap; + } + + .audio-channels { + margin-top:15px; + } + .ftue-box.results { - height: 230px !important; + height: 268px !important; padding:0; + @include border_box_sizing; + + .io, .latency { + display:none; + } + + .loopback-button-holder { + width:100%; + text-align:center; + } + a.loopback-test { + margin-top:10px; + } + + .unknown-text { + display:none; + padding:8px; + } .scoring-section { font-size:15px; @@ -214,12 +240,17 @@ &.acceptable { background-color:#cc9900; } - &.bad, &.skip { + &.bad { background-color:#660000; } - &.unknown { + &.unknown, &.skip { background-color:#999; } + &.skip { + .io-skip-msg { + display:inline; + } + } } .io-countdown { @@ -236,9 +267,71 @@ .io-skip-msg { display:none; + } - .scoring-section.skip & { - display:inline; + .io-rate { + display:none; + } + .io-var { + display:none; + } + + ul.results-text { + padding:10px 8px; + + li { + display:none + } + + &[latency-score="unknown"] { + display:none; + } + &[latency-score="good"] li.latency-good { + display:list-item; + } + &[latency-score="acceptable"] li.latency-acceptable { + display:list-item; + } + &[latency-score="bad"] li.latency-bad { + display:list-item; + } + &[io-var-score="good"] li.io-var-good { + display:list-item; + } + &[io-var-score="acceptable"] li.io-var-acceptable { + display:list-item; + } + &[io-var-score="bad"] li.io-var-bad { + display:list-item; + } + &[io-rate-score="good"] li.io-rate-good { + display:list-item; + } + &[io-rate-score="acceptable"] li.io-rate-acceptable { + display:list-item; + } + &[io-rate-score="bad"] li.io-rate-bad { + display:list-item; + } + &[scored="complete"] { + li.success { + display:list-item; + } + + &[io-rate-score="bad"], &[io-var-score="bad"], &[latency-score="bad"]{ + li.success { + display:none; + } + li.failure { + display:list-item; + } + } + + &[latency-score="unknown"] { + li.success { + display:none; + } + } } } } @@ -251,6 +344,153 @@ .wizard-step[layout-wizard-step="2"] { .wizard-step-content .wizard-step-column { width:25%; + + &:nth-of-type(2) { + width:21%; + } + + + &:nth-of-type(3) { + width:45%; + } + + + &:nth-of-type(4) { + width:9%; + } + } + + .watch-video { + margin-top:45px; + } + + .icon-instrument-select { + padding:3px 0; // to combine 24 of .current-instrument + 3x on either side + margin:0 0 15px 25px; // 15 margin-bottom to match tracks on the left + width:30px; + } + + .unassigned-channels { + min-height:240px; + overflow-y:auto; + //padding-right:18px; // to keep draggables off of scrollbar. maybe necessary + + &.drag-in-progress { + overflow-y:visible; + overflow-x:visible; + } + + &.possible-target { + border: solid 1px white; + + &.drag-hovering { + border: solid 1px #ED3618; + } + } + } + + .num { + position:absolute; + height:29px; + line-height:29px; + } + .track { + margin-bottom: 15px; + .track-target { + &.possible-target { + border-color:white; + } + &.drag-hovering { + border-color:#ED3618; + } + } + } + + + .ftue-input { + font-size:12px; + cursor: move; + padding: 4px; + border: solid 1px #999; + margin-bottom: 15px; + white-space: nowrap; + overflow:hidden; + text-align:left; + direction:rtl; + &.ui-draggable-dragging{ + margin-bottom:0; + } + + &:hover { + color:white; + font-weight: bold; + } + + /** + &:hover { + color:white; + overflow:visible; + background-color:#333; + width: auto !important; + direction:ltr; + position:absolute; + line-height:19.5px; + }*/ + } + + .track-target { + + cursor: move; + padding: 4px; + border: solid 1px #999; + margin-left: 15px; + height:20px; + overflow:hidden; + + &.drag-in-progress { + overflow:visible; + } + + .ftue-input { + padding:0; + border:0; + margin-bottom:0; + &.ui-draggable-dragging { + padding:4px; + border: solid 1px #999; + overflow:visible; + } + } + + .placeholder { + display:none; + font-size:12px; + } + + &[track-count="0"] { + .placeholder { + display:inline; + } + } + + &[track-count="2"] { + .ftue-input { + width:49%; + display:inline-block; + + &:nth-of-type(1) { + float:left; + /**&:after { + float:right; + content: ','; + padding-right:3px; + }*/ + } + &:nth-of-type(2) { + float:right; + } + } + } } } @@ -264,6 +504,9 @@ } } + .watch-video { + margin-top:97px; + } .voicechat-option { @@ -273,8 +516,17 @@ } + h3 { + padding-left:30px; + margin-top:14px; + font-weight:bold; + display:inline-block; + } + p { padding-left:30px; + margin-top:5px; + display:inline-block; } input { @@ -282,11 +534,32 @@ margin:auto; width:30px; } + + .iradio_minimal { + margin-top:15px; + display:inline-block; + } } .ftue-box { &.chat-inputs { height: 230px !important; + overflow:auto; + + p { + white-space: nowrap; + display:inline-block; + height:32px; + vertical-align: middle; + } + + .chat-input { + white-space:nowrap; + + .iradio_minimal { + display:inline-block; + } + } } .watch-video { @@ -316,8 +589,21 @@ .test-direct-monitoring { margin-top:40px; display:inline-block; + text-decoration: none; + width:90px; - span { + &.paused { + .playing { + display:none; + } + } + &.playing { + .paused { + display:none; + } + } + + .direct-monitoring-btn { padding-left:5px; font-size:16px; } @@ -330,10 +616,170 @@ .wizard-step[layout-wizard-step="5"] { + .wizard-step-content .wizard-step-column { + &:nth-of-type(1) { + width:25%; + } + &:nth-of-type(2) { + width:50%; + } + &:nth-of-type(3) { + width:25%; + } + } + + .summary { + margin-left:20px; + margin-top:22px; + } + + .watch-video { + margin-top:90px; + } + + a.start-network-test { + margin-top:20px; + } + + .network-test-score { + height:24px; + padding:10px; + color:white; + font-size:20px; + background-color:#222; + text-align:center; + margin-bottom:20px; + + &.good { + background-color: #72a43b; + } + &.acceptable { + background-color: #D6A800; + } + &.bad { + background-color: #7B0C00; + } + } + + .scoring-bar { + width:100%; + height:20px; + left:0; + position:relative; + //display:inline-block; + display:none; + + .current-score { + background-color:gray; + border-right:1px solid white; + border-top:1px solid #ccc; + border-bottom:1px solid #ccc; + border-left:1px solid #ccc; + height:20px; + display:inline-block; + position:relative; + left:0; + min-width:55px; + text-align:left; + padding-left:5px; + @include border_box_sizing; + font-size:12px; + color:white; + + .subscore { + font-size:10px; + color:white; + bottom:-15px; + right:-16px; + position:absolute; + } + } + + .good-marker { + position:absolute; + text-align:center; + left:95%; + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid #72a43b; + margin-left:-5px; + top:-7px; + } + + .good-line { + position:absolute; + height:100%; + left:95%; + width:1px; + background-color: #72a43b; + margin-left:-0.5px; + top:0; + } + } + .network-test-text { + + } + + .network-test-results { + height: 248px ! important; + @include border_box_sizing; + &.testing { + + text-align:left; + .network-test-score { + display:none; + } + + .scoring-bar { + display:inline-block; + margin-bottom:10px; + } + + .network-test-text { + //background-image: url('/assets/shared/spinner.gif'); + //background-repeat:no-repeat; + //background-position:center; + //width:128px; + //height:128px; + } + } + + &.good { + .network-test-score { + background-color: #72a43b; + } + } + + &.acceptable { + .network-test-score { + background-color: #D6A800; + } + } + + &.bad { + .network-test-score { + background-color: #7B0C00; + } + } + } } .wizard-step[layout-wizard-step="6"] { + .wizard-step-content .wizard-step-column { + &:nth-of-type(1) { + width:50%; + height:350px; + } + &:nth-of-type(2) { + width:50%; + } + ul { + margin-bottom:20px; + } + } } p { @@ -586,6 +1032,11 @@ } .audio-input { left:0px; + margin-top:30px; + + &.audio-output-showing { + margin-top:0; + } } .voice-chat-input { left:50%; diff --git a/web/app/assets/stylesheets/client/wizard/wizard.css.scss b/web/app/assets/stylesheets/client/wizard/wizard.css.scss new file mode 100644 index 000000000..02ef8e802 --- /dev/null +++ b/web/app/assets/stylesheets/client/wizard/wizard.css.scss @@ -0,0 +1,45 @@ +@import "client/common.css.scss"; +@charset "UTF-8"; + +.dialog { + .wizard-buttons { + position: absolute; + bottom: 0; + width:100%; + @include border_box_sizing; + padding-right:45px; + padding-bottom:15px; + + } + + .wizard-buttons-holder { + float:right; + } + + .wizard-step { + display:none; + position:relative; + } + + .wizard-step-content { + margin-top:20px; + padding-bottom:12px; + + .wizard-step-column{ + position:relative; + float:left; + vertical-align:top; + @include border_box_sizing; + padding-right:12px; + height:300px; + } + + .wizard-step-column:last-child { + //padding-right:0; + } + + p:nth-of-type(1) { + margin-top:0; + } + } +} \ No newline at end of file diff --git a/web/app/assets/stylesheets/easydropdown_jk.css.scss b/web/app/assets/stylesheets/easydropdown_jk.css.scss index ed51ce5a2..4b2ff52b6 100644 --- a/web/app/assets/stylesheets/easydropdown_jk.css.scss +++ b/web/app/assets/stylesheets/easydropdown_jk.css.scss @@ -32,6 +32,7 @@ body.jam { left: -1px; top: 100%; margin-top: -1px; + margin-left:1px; background: #fff; border: 1px solid #ccc; border-top: 1px solid #eee; diff --git a/web/app/assets/stylesheets/web/welcome.css.scss b/web/app/assets/stylesheets/web/welcome.css.scss index e78d9df34..c7ac49fbb 100644 --- a/web/app/assets/stylesheets/web/welcome.css.scss +++ b/web/app/assets/stylesheets/web/welcome.css.scss @@ -13,6 +13,12 @@ body.web { } } } + .share_links { + position: absolute; + top: 116px; + left: 1100px; + z-index: 10; + } .buzz { width: 300px; diff --git a/web/app/controllers/api_invited_users_controller.rb b/web/app/controllers/api_invited_users_controller.rb index 87210a778..33a118fad 100644 --- a/web/app/controllers/api_invited_users_controller.rb +++ b/web/app/controllers/api_invited_users_controller.rb @@ -1,4 +1,4 @@ - class ApiInvitedUsersController < ApiController +class ApiInvitedUsersController < ApiController # have to be signed in currently to see this screen before_filter :api_signed_in_user diff --git a/web/app/controllers/api_latency_testers_controller.rb b/web/app/controllers/api_latency_testers_controller.rb new file mode 100644 index 000000000..19b4293ac --- /dev/null +++ b/web/app/controllers/api_latency_testers_controller.rb @@ -0,0 +1,15 @@ +class ApiLatencyTestersController < ApiController + + # have to be signed in currently to see this screen + before_filter :api_signed_in_user + + respond_to :json + + def match + # some day we can find the best latency tester to test against, now there is only one. + @latency_tester = LatencyTester.select_latency_tester + + respond_with_model(@latency_tester) + end +end + diff --git a/web/app/controllers/api_scoring_controller.rb b/web/app/controllers/api_scoring_controller.rb index 5ba61122d..3a577a95e 100644 --- a/web/app/controllers/api_scoring_controller.rb +++ b/web/app/controllers/api_scoring_controller.rb @@ -7,13 +7,12 @@ class ApiScoringController < ApiController clientid = params[:clientid] if clientid.nil? then render :json => {message: 'clientid not specified'}, :status => 400; return end - conn = Connection.where(client_id: clientid).first + conn = Connection.where(client_id: clientid, user_id: current_user.id).first if conn.nil? then render :json => {message: 'session not found'}, :status => 404; return end - if !current_user.id.eql?(conn.user.id) then render :json => {message: 'session not owned by user'}, :status => 403; return end + # if !current_user.id.eql?(conn.user.id) then render :json => {message: 'session not owned by user'}, :status => 403; return end - # todo this method is a stub #puts "ApiScoringController#work(#{clientid}) => locidispid #{c.locidispid}" - result_client_id = JamRuby::GetWork.get_work(conn.locidispid) + result_client_id = JamRuby::GetWork.get_work(conn.locidispid, conn.addr) #result_client_id = clientid+'peer' render :json => {:clientid => result_client_id}, :status => 200 @@ -23,12 +22,11 @@ class ApiScoringController < ApiController clientid = params[:clientid] if clientid.nil? then render :json => {message: 'clientid not specified'}, :status => 400; return end - conn = Connection.where(client_id: clientid).first + conn = Connection.where(client_id: clientid, user_id: current_user.id).first if conn.nil? then render :json => {message: 'session not found'}, :status => 404; return end - if !current_user.id.eql?(conn.user.id) then render :json => {message: 'session not owned by user'}, :status => 403; return end + # if !current_user.id.eql?(conn.user.id) then render :json => {message: 'session not owned by user'}, :status => 403; return end - # todo this method is a stub - result_client_ids = JamRuby::GetWork.get_work_list(conn.locidispid) + result_client_ids = JamRuby::GetWork.get_work_list(conn.locidispid, conn.addr) #result_client_ids = [clientid+'peer1', clientid+'peer2'] render :json => {:clientids => result_client_ids}, :status => 200 @@ -37,11 +35,13 @@ class ApiScoringController < ApiController def record # aclientid, aAddr, bclientid, bAddr, score returns nothing #puts "================= record #{params.inspect}" + aclientid = params[:aclientid] aip_address = params[:aAddr] bclientid = params[:bclientid] bip_address = params[:bAddr] score = params[:score] + score_data = params.to_s if aclientid.nil? then render :json => {message: 'aclientid not specified'}, :status => 400; return end if aip_address.nil? then render :json => {message: 'aAddr not specified'}, :status => 400; return end @@ -61,10 +61,10 @@ class ApiScoringController < ApiController if !score.is_a? Numeric then render :json => {message: 'score not valid numeric'}, :status => 400; return end - aconn = Connection.where(client_id: aclientid).first + aconn = Connection.where(client_id: aclientid, user_id: current_user.id).first if aconn.nil? then render :json => {message: 'a\'s session not found'}, :status => 404; return end if aAddr != aconn.addr then render :json => {message: 'a\'s session addr does not match aAddr'}, :status => 403; return end - if !current_user.id.eql?(aconn.user.id) then render :json => {message: 'a\'s session not owned by user'}, :status => 403; return end + # if !current_user.id.eql?(aconn.user.id) then render :json => {message: 'a\'s session not found'}, :status => 403; return end bconn = Connection.where(client_id: bclientid).first if bconn.nil? then render :json => {message: 'b\'s session not found'}, :status => 404; return end @@ -82,7 +82,7 @@ class ApiScoringController < ApiController if bisp.nil? or bloc.nil? then render :json => {message: 'b\'s location or isp not found'}, :status => 404; return end blocidispid = bloc.locid*1000000+bisp.coid - JamRuby::Score.createx(alocidispid, aclientid, aAddr, blocidispid, bclientid, bAddr, score.ceil, nil) + JamRuby::Score.createx(alocidispid, aclientid, aAddr, blocidispid, bclientid, bAddr, score.ceil, nil, score_data) render :json => {}, :status => 200 end diff --git a/web/app/controllers/api_search_controller.rb b/web/app/controllers/api_search_controller.rb index 81baf7b6c..468371a79 100644 --- a/web/app/controllers/api_search_controller.rb +++ b/web/app/controllers/api_search_controller.rb @@ -7,15 +7,18 @@ class ApiSearchController < ApiController def index if 1 == params[Search::PARAM_MUSICIAN].to_i || 1 == params[Search::PARAM_BAND].to_i + # puts "================== params #{params.to_s}" query = params.clone query[:remote_ip] = request.remote_ip - if 1 == params[Search::PARAM_MUSICIAN].to_i - @search = Search.musician_filter(query, current_user) + if 1 == query[Search::PARAM_MUSICIAN].to_i + clientid = query[:clientid] + conn = (clientid ? Connection.where(client_id: clientid, user_id: current_user.id).first : nil) + # puts "================== query #{query.inspect}" + @search = Search.musician_filter(query, current_user, conn) else @search = Search.band_filter(query, current_user) end respond_with @search, responder: ApiResponder, :status => 200 - elsif 1 == params[Search::PARAM_SESSION_INVITE].to_i @search = Search.session_invite_search(params[:query], current_user) else diff --git a/web/app/controllers/clients_controller.rb b/web/app/controllers/clients_controller.rb index 637688b11..928715adf 100644 --- a/web/app/controllers/clients_controller.rb +++ b/web/app/controllers/clients_controller.rb @@ -3,6 +3,10 @@ class ClientsController < ApplicationController include ClientHelper include UsersHelper + + AUTHED = %W{friend} + + def index # we want to enforce that /client is always the client view prefix @@ -15,7 +19,9 @@ class ClientsController < ApplicationController render :layout => 'client' end - AUTHED = %W{friend} + def latency_tester + render :layout => 'client' + end def auth_action if current_user @@ -31,5 +37,4 @@ class ClientsController < ApplicationController redirect_to client_url end end - end diff --git a/web/app/controllers/users_controller.rb b/web/app/controllers/users_controller.rb index d380c6b34..8fa2cc381 100644 --- a/web/app/controllers/users_controller.rb +++ b/web/app/controllers/users_controller.rb @@ -370,20 +370,35 @@ class UsersController < ApplicationController end def endorse - if uu = User.where(['id = ? AND first_liked_us IS NULL',params[:id]]).limit(1).first - uu.first_liked_us = Time.now + if uu = current_user || + uu = User.where(['id = ? AND first_social_promoted_at IS NULL',params[:id]]).limit(1).first + uu.first_social_promoted_at = Time.now uu.save! - end if params[:id].present? + end if params[:id].present? && (service=params[:service]).present? - url, service = 'http://www.jamkazam.com', params[:service] + service ||= 'facebook' + url = CGI::escape('http://www.jamkazam.com') + txt = CGI::escape('Check out JamKazam -- Play music together over the Internet as if in the same room') if 'twitter'==service - url = 'https://twitter.com/jamkazam' + url = "https://twitter.com/intent/tweet?text=#{txt}&url=#{url}" elsif 'facebook'==service - url = 'https://www.facebook.com/JamKazam' + url = "http://www.facebook.com/sharer/sharer.php?u=#{url}&t=#{txt}" elsif 'google'==service - url = 'https://plus.google.com/u/0/106619885929396862606/about' + url = "https://plus.google.com/share?url=#{url}" + end + if 'email'==params[:src] + js =< +$(function() { + JK.GA.trackJKSocial(JK.GA.Categories.jkLike, '#{service}', 'email'); + window.location = "#{url}"; +}); + +JS + render :inline => js, :layout => 'landing' + else + redirect_to url end - redirect_to url end private diff --git a/web/app/helpers/client_helper.rb b/web/app/helpers/client_helper.rb index 045b50636..f8f52caf6 100644 --- a/web/app/helpers/client_helper.rb +++ b/web/app/helpers/client_helper.rb @@ -29,10 +29,24 @@ module ClientHelper gon.websocket_gateway_uri = Rails.application.config.websocket_gateway_uri end + if Rails.env == "development" + # if in development mode, we assume you are running websocket-gateway + # on the same host as you hit your server. + gon.websocket_gateway_trusted_uri = "ws://" + request.host + ":6768/websocket"; + else + # but in any other mode, just use config + gon.websocket_gateway_trusted_uri = Rails.application.config.websocket_gateway_trusted_uri + end + gon.check_for_client_updates = Rails.application.config.check_for_client_updates gon.fp_apikey = Rails.application.config.filepicker_rails.api_key gon.fp_upload_dir = Rails.application.config.filepicker_upload_dir gon.allow_force_native_client = Rails.application.config.allow_force_native_client + gon.ftue_io_wait_time = Rails.application.config.ftue_io_wait_time + gon.ftue_packet_rate_treshold = Rails.application.config.ftue_packet_rate_treshold + 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 # is this the native client or browser? @nativeClient = is_native_client? diff --git a/web/app/views/api_latency_testers/match.rabl b/web/app/views/api_latency_testers/match.rabl new file mode 100644 index 000000000..c88daa8ad --- /dev/null +++ b/web/app/views/api_latency_testers/match.rabl @@ -0,0 +1,3 @@ +object @latency_tester + +extends "api_latency_testers/show" \ No newline at end of file diff --git a/web/app/views/api_latency_testers/show.rabl b/web/app/views/api_latency_testers/show.rabl new file mode 100644 index 000000000..fd955b185 --- /dev/null +++ b/web/app/views/api_latency_testers/show.rabl @@ -0,0 +1,3 @@ +object @latency_tester + +attribute :id, :client_id \ No newline at end of file diff --git a/web/app/views/api_search/index.rabl b/web/app/views/api_search/index.rabl index 16c1f85fc..fd53ba9c2 100644 --- a/web/app/views/api_search/index.rabl +++ b/web/app/views/api_search/index.rabl @@ -43,7 +43,7 @@ if @search.musicians_filter_search? end child(:results => :musicians) { - 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, :joined_score node :is_friend do |musician| @search.is_friend?(musician) diff --git a/web/app/views/api_users/notification_index.rabl b/web/app/views/api_users/notification_index.rabl index 69b17b2ba..23852b5fd 100644 --- a/web/app/views/api_users/notification_index.rabl +++ b/web/app/views/api_users/notification_index.rabl @@ -16,19 +16,19 @@ end # this has to be flat like this so this payload is the same as the protocol buffer (see initializeActions in sidebar.js) node :fan_access do |n| - unless n.session_id.blank? - n.session.fan_access + unless n.music_session.nil? + n.music_session.fan_access end end node :musician_access do |n| - unless n.session_id.blank? - n.session.musician_access + unless n.music_session.nil? + n.music_session.musician_access end end node :approval_required do |n| - unless n.session_id.blank? - n.session.approval_required + unless n.music_session.nil? + n.music_session.approval_required end end diff --git a/web/app/views/clients/_account.html.erb b/web/app/views/clients/_account.html.erb index b9baa122f..7f4acb741 100644 --- a/web/app/views/clients/_account.html.erb +++ b/web/app/views/clients/_account.html.erb @@ -25,12 +25,29 @@
    -
    + {% if (data.musician) { %} +
    - <% if current_user && current_user.musician? %> @@ -99,22 +116,7 @@
    -
    - - - - - -
    - UPDATE -
    -
    - <% end %> + {% } %} diff --git a/web/app/views/clients/_account_audio_profile.html.erb b/web/app/views/clients/_account_audio_profile.html.erb index 57588f1d7..72570a67f 100644 --- a/web/app/views/clients/_account_audio_profile.html.erb +++ b/web/app/views/clients/_account_audio_profile.html.erb @@ -27,7 +27,7 @@
    - + @@ -44,11 +44,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/web/app/views/clients/_iconInstrumentSelect.html.haml b/web/app/views/clients/_iconInstrumentSelect.html.haml new file mode 100644 index 000000000..50ba7271d --- /dev/null +++ b/web/app/views/clients/_iconInstrumentSelect.html.haml @@ -0,0 +1,12 @@ +%script{type: 'text/template', id: 'template-icon-instrument-select'} + .icon-instrument-select + .current-instrument.none + %a.arrow-down + %ul + = '{% _.each(data.instruments, function(instrument, instrumentId) { %}' + %li{'data-instrument-id' => '{{instrumentId}}'} + %a{href:'#'} + %img{src: '{{instrument.asset}}'} + = '{{instrument.name}}' + = '{% }) %}' + diff --git a/web/app/views/clients/_musician_filter.html.erb b/web/app/views/clients/_musician_filter.html.erb index 429c54659..a32ae7485 100644 --- a/web/app/views/clients/_musician_filter.html.erb +++ b/web/app/views/clients/_musician_filter.html.erb @@ -13,12 +13,12 @@ <% end -%> <%= content_tag(:div, :class => 'filter-element wrapper') do -%> - <%= content_tag(:div, 'Within', :class => 'filter-element desc') %> + <%= content_tag(:div, 'Latency:', :class => 'filter-element desc') %> <%= content_tag(:div, :class => 'query-distance-params') do -%> - <%= select_tag('musician_query_distance', options_for_select(Search::M_DISTANCE_OPTS, Search::M_MILES_DEFAULT)) %> + <%= select_tag('musician_query_score', options_for_select(Search::M_SCORE_OPTS, Search::M_SCORE_DEFAULT)) %> <% end -%> <%= content_tag(:div, :class => 'filter-element') do -%> - miles of <%= content_tag(:span, current_user ? current_user.current_city(request.remote_ip) : '', :id => 'musician-filter-city') %> + to <%= content_tag(:span, current_user ? current_user.current_city(request.remote_ip) : '', :id => 'musician-filter-city') %> <% end -%> <% end -%> <%= content_tag(:div, diff --git a/web/app/views/clients/_musicians.html.erb b/web/app/views/clients/_musicians.html.erb index a5ef62b25..599710a6c 100644 --- a/web/app/views/clients/_musicians.html.erb +++ b/web/app/views/clients/_musicians.html.erb @@ -30,6 +30,15 @@
    + +
    FOLLOWING:
    - <% [:twitter, :facebook, :google].each do |src| %> - <%= link_to(image_tag("http://www.jamkazam.com/assets/content/icon_#{src}.png", :style => "vertical-align:top"), "http://www.jamkazam.com/endorse/@USERID/#{src}") %>  + <% [:twitter, :facebook, :google].each do |site| %> + <%= link_to(image_tag("http://www.jamkazam.com/assets/content/icon_#{site}.png", :style => "vertical-align:top"), "http://www.jamkazam.com/endorse/@USERID/#{site}?src=email") %>  <% end %>
    NAMEDELETEACTIONS
    {musician_follow_template}
    diff --git a/web/app/views/clients/_rateSession.html.erb b/web/app/views/clients/_rateSession.html.erb index fa87832f5..abc9d2bb5 100644 --- a/web/app/views/clients/_rateSession.html.erb +++ b/web/app/views/clients/_rateSession.html.erb @@ -1,4 +1,4 @@ -
    +
    <%= image_tag "shared/icon_session.png", {:height => 19, :width => 19, :class => "content-icon"} %> diff --git a/web/app/views/clients/_rsvpCancelDialog.html.haml b/web/app/views/clients/_rsvpCancelDialog.html.haml index e69de29bb..dbe7127c4 100644 --- a/web/app/views/clients/_rsvpCancelDialog.html.haml +++ b/web/app/views/clients/_rsvpCancelDialog.html.haml @@ -0,0 +1,29 @@ +.dialog.dialog-overlay-sm{layout: 'dialog', 'layout-id' => 'rsvp-cancel-dialog', id: 'rsvp-cancel-dialog'} + .content-head + = image_tag 'content/icon_checkmark_circle.png', :alt => "", :class => "content-icon", :width => "20", :height => "20" + %h1 cancel rsvp + .dialog-inner + %h2 SESSION + %span.session-name + %br/ + %span.scheduled-start + %br/ + %span.schedule-recurrence + %br/ + %br/ + .error{:style => 'display:none'} + %input{:type => 'radio', :name => 'cancel', :value => 'yes', :checked => true} Cancel RSVP just for this one session + %br/ + %input{:type => 'radio', :name => 'cancel', :value => 'all'} Cancel RSVP for all future sessions + %br/ + %br/ + Enter a message to the other musicians in the session (optional): + %textarea.w95.p5.f15{id: 'txtComment', rows: '2', placeholder: 'Enter a comment...'} + %br/ + %br/ + .left + %a.button-orange{:href => 'TBD', :rel => 'external', :target => '_blank'} HELP + .right + %a.button-grey{:id => 'btnCancel', 'layout-action' => 'close'} CANCEL + %a.button-orange{:id => 'btnCancelRsvp'} CANCEL RSVP + %br{:clear => "all"}/ \ No newline at end of file diff --git a/web/app/views/clients/_rsvpSubmitDialog.html.haml b/web/app/views/clients/_rsvpSubmitDialog.html.haml index 49e7b2d9c..319313bc9 100644 --- a/web/app/views/clients/_rsvpSubmitDialog.html.haml +++ b/web/app/views/clients/_rsvpSubmitDialog.html.haml @@ -10,8 +10,9 @@ %br/ %span.schedule-recurrence %br/ - %br/ + %br/ %span.slot-instructions Check the box(es) next to the track(s) you want to play in the session: + .error{:style => 'display:none'} You must select at least 1 instrument. .rsvp-instruments %br/ Enter a message to the other musicians in the session (optional): @@ -19,8 +20,8 @@ %br/ %br/ .left - %a.button-orange{:href => 'TBD', :rel => 'external'} HELP + %a.button-orange{:href => 'TBD', :rel => 'external', :target => '_blank'} HELP .right - %a.button-grey{:id => 'btnCancel', :href => 'TBD', 'layout-action' => 'close'} CANCEL - %a.button-orange{:id => 'btnSubmit', :href => 'TBD'} SUBMIT RSVP + %a.button-grey{:id => 'btnCancel', 'layout-action' => 'close'} CANCEL + %a.button-orange{:id => 'btnSubmitRsvp'} SUBMIT RSVP %br{:clear => "all"}/ \ No newline at end of file diff --git a/web/app/views/clients/_sessionSettings.html.erb b/web/app/views/clients/_sessionSettings.html.erb deleted file mode 100644 index 6dbc3d68e..000000000 --- a/web/app/views/clients/_sessionSettings.html.erb +++ /dev/null @@ -1,75 +0,0 @@ - -
    - - -
    - <%= image_tag "content/icon_settings_lg.png", - {:width => 18, :height => 18, :class => "content-icon"} %> -

    update session settings

    -
    - -
    -
    - - -
    - - - Genre:
    - -
    - <%= render "genreSelector" %> -
    -
    -
    - -
    - Musician Access:
    -
    - -  Open  
    -  By Approval -
    -
    - - Fan Access:
    - -
    -  Chat  
    -  No Fan Chat - -
    -
    - - -
    - - - Description:
    - -
    -
    - - -
    - -
    - -
    -
    - - -
    - -
    diff --git a/web/app/views/clients/_sessionSettings.html.haml b/web/app/views/clients/_sessionSettings.html.haml new file mode 100644 index 000000000..2fb2ef54b --- /dev/null +++ b/web/app/views/clients/_sessionSettings.html.haml @@ -0,0 +1,61 @@ + +.dialog{:layout => 'dialog', 'layout-id' => 'session-settings', :id => 'session-settings', :width => '600px', :height => '800px'} + + .content-head + = image_tag "content/icon_settings_lg.png", :width => 18, :height => 18, :class => "content-icon" + %h1 session properties + + .dialog-inner + %form{:id => "session-settings-dialog"} + + %input{:type => "hidden", :name => "id"} + + .left.mr35 + + .left.ib + Genre: + .right.w75.ib.mb10{:id => "session-settings-genre"} + = render "genreSelector" + + .clearall.left.w25.ib.mb10 + Name: + .right.w75.ib.mb10 + %input{:type => 'text', :name => 'session_name'} + + .clearall.left.w25.ib.mb10 + Description: + .right.w75.ib.mb10 + %textarea.settings-session-description{:rows => 5, :name => "description"} + + .clearall.left.w25.ib.mb10 + Language: + .right.w75.ib.mb10 + %select{:name => 'language'} + %option{:value => 'en'} English + + .clearall.left.w20.ib.mb10 + Musician Access: + .right.w75.ib.mb10 + %select{:id => "session-settings-musician-access", :name => "musician_access"} + %option{:value => "true"} Public + %option{:value => "false"} Private + + .clearall.left.w25.ib.mb10 + Fan Access: + .right.w75.ib.mb10 + %select{:name => "fan_access"} + %option{:value => "true"} Public + %option{:value => "false"} Private + + .clearall.left.w25.ib.mb10 + Notation Files: + .right.w75.ib.mb10 + List of existing notation files goes here + + .clearall.right + %a.button-orange{:href => 'TBD', :rel => 'external'} HELP + %a.button-grey{'layout-action' => "close"} CANCEL + %a.button-orange{:id => "session-settings-dialog-submit", :href => "#"} UPDATE SETTINGS + + %br/ + %br{:clear => 'all'}/ \ No newline at end of file diff --git a/web/app/views/clients/_sidebar.html.erb b/web/app/views/clients/_sidebar.html.erb index 8836beb67..5a48671e6 100644 --- a/web/app/views/clients/_sidebar.html.erb +++ b/web/app/views/clients/_sidebar.html.erb @@ -102,7 +102,7 @@
    - Chat is available during session is connected. + Chat is available when in session.
    diff --git a/web/app/views/clients/_web_filter.html.erb b/web/app/views/clients/_web_filter.html.erb index d4b41c699..5beed3237 100644 --- a/web/app/views/clients/_web_filter.html.erb +++ b/web/app/views/clients/_web_filter.html.erb @@ -47,6 +47,16 @@ <%= content_tag(:div, 'Show:', :class => 'filter-element desc') %> <%= select_tag("#{filter_label}_show", options_for_select(Search::SHOW_OPTS), {:class => "easydropdown"}) %> + <% elsif :musician == filter_label %> + + <%= content_tag(:div, 'Latency:', :class => 'filter-element desc') %> + <%= content_tag(:div, :class => 'query-distance-params') do -%> + <%= select_tag("musician_query_score", options_for_select(Search::M_SCORE_OPTS, Search::M_SCORE_DEFAULT), {:class => 'easydropdown'}) %> + <% end -%> + <%= content_tag(:div, :class => 'filter-element desc') do -%> + to <%= content_tag(:span, current_user ? current_user.current_city(request.remote_ip) : '', :id => "musician-filter-city") %> + <% end -%> + <% else %> <%= content_tag(:div, 'Within', :class => 'filter-element desc') %> @@ -57,7 +67,7 @@ <%= content_tag(:div, :class => 'filter-element desc') do -%> miles of <%= content_tag(:span, current_user ? current_user.current_city(request.remote_ip) : '', :id => "#{filter_label}-filter-city") %> <% end -%> - + <% end %> <% end -%> diff --git a/web/app/views/clients/gear/_buttons.html.haml b/web/app/views/clients/gear/_buttons.html.haml index 5f3209ce9..3ab9f4629 100644 --- a/web/app/views/clients/gear/_buttons.html.haml +++ b/web/app/views/clients/gear/_buttons.html.haml @@ -4,9 +4,9 @@ - total_steps = 7 -.ftue-buttons - .ftue-buttons-holder - %a.button-grey{href: '#'} HELP +.wizard-buttons + .wizard-buttons-holder + %a.button-grey.btn-help{href: '#'} HELP - if step > 0 && step != total_steps %a.button-orange.btn-back{href:'#'} BACK - if step != total_steps diff --git a/web/app/views/clients/gear/_gear_wizard.html.haml b/web/app/views/clients/gear/_gear_wizard.html.haml index 57f0ef1f8..56fd3cd0a 100644 --- a/web/app/views/clients/gear/_gear_wizard.html.haml +++ b/web/app/views/clients/gear/_gear_wizard.html.haml @@ -31,22 +31,29 @@ %li Configure interface settings. %li View test results. .center - %a.button-orange.watch-video.audio-input{href:'#', rel:'external'} WATCH VIDEO - %a.button-orange.watch-video.audio-output{href:'#', rel:'external'} WATCH VIDEO + %a.button-orange.watch-video.audio-input{href:'#', rel:'external'} + WATCH VIDEO + %br + %span.video-type (FOR INPUT DEVICE) + %a.button-orange.watch-video.audio-output{href:'#', rel:'external'} + WATCH VIDEO + %br + %span.video-type (FOR OUTPUT DEVICE) .wizard-step-column %h2 Audio Input Device %select.w100.select-audio-input-device %option None - %h2 Audio Input Ports + %h2.audio-channels Audio Input Ports .ftue-box.list.ports.input-ports - %a.button-orange.asio-settings-btn ASIO SETTINGS... + %a.button-orange.asio-settings-input-btn ASIO SETTINGS... %a.button-orange.resync-btn RESYNC .wizard-step-column %h2 Audio Output Device %select.w100.select-audio-output-device %option Same as input - %h2 Audio Output Ports + %h2.audio-channels Audio Output Ports .ftue-box.list.ports.output-ports + %a.button-orange.asio-settings-output-btn ASIO SETTINGS... .frame-and-buffers .framesize %h2 Frame @@ -55,7 +62,7 @@ %option{val:'5'} 5 %option{val:'10'} 10 .buffers - %h2 Buffers In/Out + %h2 Buffer In/Out %select.select-buffer-in %option{val:'0'} 0 %option{val:'1'} 1 @@ -91,13 +98,33 @@ .p5 .io I/O %span.io-skip-msg - Skipped + Not Tested %span.io-countdown %span.secs seconds left - %span.io-rate-score - %span.io-var-score - + %span.io-rate< + Rate= + %span.io-rate-score> + %span.io-var< + Var= + %span.io-var-score> + .clearall + %ul.results-text + %li.latency-good Your latency is good. + %li.latency-acceptable Your latency is acceptable. + %li.latency-bad Your latency is poor. + %li.io-rate-good Your I/O rate is good. + %li.io-rate-acceptable Your I/O rate is acceptable. + %li.io-rate-bad Your I/O rate is poor. + %li.io-var-good Your I/O variance is good. + %li.io-var-acceptable Your I/O variance is acceptable. + %li.io-var-bad Your I/O variance is poor. + %li.success You may proceed to the next step. + %li.failure We're sorry, but your audio gear has failed. Please watch video or click HELP button below. + .unknown-text + %div We cannot accurately predict the latency of your audio gear. To proceed, you must run an audio loopback test. Click button below to do this. + %div.loopback-button-holder + %a.button-orange.loopback-test{href:'#'} RUN LOOPBACK TEST .clearall .wizard-step{ 'layout-wizard-step' => "2", 'dialog-title' => "Configure Tracks", 'dialog-purpose' => "ConfigureTracks" } @@ -114,17 +141,20 @@ %li Select the instrument for each track. .center %a.button-orange.watch-video{href:'#'} WATCH VIDEO - .wizard-step-column + .wizard-step-column.no-selection-range %h2 Unassigned Ports - .wizard-step-column + .unassigned-channels + .wizard-step-column.no-selection-range %h2 Track Input Port(s) - .wizard-step-column - %h2 Track Instrument + .tracks + .wizard-step-column.no-selection-range + %h2 Instrument + .instruments .wizard-step{ 'layout-wizard-step' => "3", 'dialog-title' => "Configure Voice Chat", 'dialog-purpose' => "ConfigureVoiceChat" } .ftuesteps .clearall - .help-text In this step, you will select, configure, and test your audio gear. Please watch the video for best instructions. + .help-text In this step, you may set up a microphone to use for voice chat. Please watch the video for best instructions. .wizard-step-content .wizard-step-column %h2 Instructions @@ -137,12 +167,14 @@ %h2 Select Voice Chat Option .voicechat-option.reuse-audio-input %input{type:"radio", name: "voicechat", checked:"checked"} + %h3 Use Music Microphone %p I am already using a microphone to capture my vocal or instrumental music, so I can talk with other musicians using that microphone .voicechat-option.use-chat-input - %input{type:"radio", name: "voicechat", checked:"unchecked"} + %input{type:"radio", name: "voicechat"} + %h3 Use Chat Microphone %p I am not using a microphone for acoustic instruments or vocals, so use the input selected to the right for voice chat during my sessions .wizard-step-column - %h2 Track Input Port(s) + %h2 Voice Chat Input .ftue-box.chat-inputs @@ -163,25 +195,85 @@ .wizard-step-column .help-content When you have fully turned off the direct monitoring control (if any) on your audio interface, - please click the Play busson below. If you hear the audio clearly, then your settings are correct, + please click the Play button below. If you hear the audio clearly, then your settings are correct, and you can move ahead to the next step. If you use your audio interface for recording, and use the direct monitoring feature for recording, please note that you will need to remember to turn this feature off every time that you use the JamKazam service. .center - %a.ftue-box.test-direct-monitoring - %img{src:'assets/content/icon_playbutton.png', width:20, height:20, align:'top'} - %span Play + %a.ftue-box.test-direct-monitoring.paused.no-selection-range + .playing + %img{src:'assets/content/icon_pausebutton.png', width:20, height:20, align:'top'} + %span.direct-monitoring-btn Pause + .paused + %img{src:'assets/content/icon_playbutton.png', width:20, height:20, align:'top'} + %span.direct-monitoring-btn Play .wizard-step{ 'layout-wizard-step' => "5", 'dialog-title' => "Test Router & Network", 'dialog-purpose' => "TestRouterNetwork" } .ftuesteps + .clearall + .help-text In this step, you will test your router and Internet connection to ensure that you can play in online sessions, and to see how many musicians can be in a session with you based on your internet connection. + .wizard-step-content + .wizard-step-column + %h2 Instructions + .ftue-box.instructions + %ul + %li Check that computer is connected to router using Ethernet cable. + %li Click Start Network Test button. + %li View test results. + .center + %a.button-orange.watch-video{href:'#'} WATCH VIDEO + .wizard-step-column + .summary + %p Ensure that your computer is connected to your home router using an Ethernet cable rather than using Wi-Fi wireless access. If necessary, find or purchase a long Ethernet cable, up to 100 ft. + %p Then click on the Start Network Test button below. + .center + %a.button-orange.start-network-test{href:'#'} START NETWORK TEST + .wizard-step-column + %h2 Test Results + .network-test-results.ftue-box + .scoring-bar + .current-score + testing... + .subscore + .good-marker + .good-line + .network-test-score + .scored-clients + .network-test-text .wizard-step{ 'layout-wizard-step' => "6", 'dialog-title' => "Success!", 'dialog-purpose' => "Success" } .ftuesteps - - - .ftue-buttons + .clearall + .wizard-step-content + .wizard-step-column + %p Congratulations! You are now ready to create, join, and play in online sessions with other JamKazam musicians! + %p Please feel free to watch any of the videos to the right for information on how to connect and play with other musicians, and how to get the most out of JamKazam. When you're ready to go, click the Close button. + %p Have fun and thanks for joining us! + %p — Team JamKazam + .wizard-step-column + %h2 Tutorial Videos + %ul + %li + %a Creating a Session + %li + %a Finding a Session + %li + %a Playing in a Session + %li + %a Connecting with Other Musicians + %li + %a Making and Sharing Recordings + %li + %a Broadcasting Your Sessions + %h2 Other Valuable Resource Links + %ul + %li + %a JamKazam Support Center + %li + %a JamKazam Community Forum + .wizard-buttons %script{type: 'text/template', id: 'template-ftuesteps'} .ftuesteps-inner @@ -201,10 +293,10 @@ .ftue-step-title Success! -%script{type: 'text/template', id: 'template-ftue-buttons'} - .ftue-buttons-holder +%script{type: 'text/template', id: 'template-wizard-buttons'} + .wizard-buttons-holder %a.button-grey.btn-cancel{href:'#', 'layout-action' => 'close'} CANCEL - %a.button-grey{href: '#'} HELP + %a.button-grey.btn-help{href: '#'} HELP %a.button-orange.btn-back{href:'#'} BACK %a.button-orange.btn-next{href:'#'} NEXT %a.button-orange.btn-close{href:'#', 'layout-action' => 'close'} CLOSE @@ -215,5 +307,17 @@ %span = '{{data.name}}' +%script{type: 'text/template', id: 'template-assignable-port'} + .ftue-input{'data-input-id' => '{{data.id}}'} {{data.name}} +%script{type: 'text/template', id: 'template-track-target'} + .track{'data-num' => '{{data.num}}'} + .num {{data.num + 1}}: + .track-target{'data-num' => '{{data.num}}', 'track-count' => 0} + %span.placeholder None +%script{type: 'text/template', id: 'template-chat-input'} + .chat-input + %input{type:"radio", name: "chat-device", 'data-channel-id' => '{{data.id}}', 'data-channel-name' => '{{data.name}}'} + %p + = '{{data.name}}' diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index d405814a8..e02251a4f 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -18,6 +18,7 @@ <%= render "vu_meters" %> <%= render "ftue" %> <%= render "jamServer" %> +<%= render "iconInstrumentSelect" %> <%= render "clients/gear/gear_wizard" %> <%= render "terms" %> <%= render "leaveSessionWarning" %> @@ -269,6 +270,12 @@ JK.initJamClient(); + // latency_tester does not want to be here + if(window.jamClient.getOperatingMode() == "server") { + window.location = "/latency_tester"; + return; + } + // Let's get things rolling... if (JK.currentUserId) { diff --git a/web/app/views/clients/latency_tester.html.haml b/web/app/views/clients/latency_tester.html.haml new file mode 100644 index 000000000..27e632c41 --- /dev/null +++ b/web/app/views/clients/latency_tester.html.haml @@ -0,0 +1,73 @@ += render :partial => "banner" += render :partial => "clients/banners/disconnected" += render :partial => "jamServer" + +:javascript + $(function() { + JK = JK || {}; + + JK.logger.proxy_logs_to_backend = true; + + JK.root_url = "#{root_url}" + + // if in development mode, we assume you are running websocket-gateway + // on the same host as you hit your server. + JK.websocket_gateway_uri = #{Rails.env == "development" ? '"ws://" + location.hostname + ":6768/websocket"' : 'gon.websocket_gateway_trusted_uri'}; + + if (console) { console.log("websocket_gateway_uri:" + JK.websocket_gateway_uri); } + + + // If no trackVolumeObject (when not running in native client) + // create a fake one. + if (!(window.trackVolumeObject)) { + window.trackVolumeObject = { + bIsMediaFile: false, + broadcast: false, + clientID: "", + instrumentID: "", + master: false, + monitor: false, + mute: false, + name: "", + objectName: "", + record: false, + volL: 0, + volR: 0, + wigetID: "" + }; + } + + + // Some things can't be initialized until we're connected. Put them here. + function _initAfterConnect(connected) { + if (this.didInitAfterConnect) return; + this.didInitAfterConnect = true + + if(!connected) { + jamServer.initiateReconnect(null, true); + } + } + + JK.app = JK.JamKazam(); + var jamServer = new JK.JamServer(JK.app, function(event_type) {}); + jamServer.initialize(); + + // If no jamClient (when not running in native client) + // create a fake one. + if (!(window.jamClient)) { + var p2pMessageFactory = new JK.FakeJamClientMessages(); + window.jamClient = new JK.FakeJamClient(JK.app, p2pMessageFactory); + window.jamClient.SetFakeRecordingImpl(new JK.FakeJamClientRecordings(JK.app, jamClient, p2pMessageFactory)); + } + + //JK.app.initialize(); + + JK.JamServer.connect() // singleton here defined in JamServer.js + .done(function() { + _initAfterConnect(true); + }) + .fail(function() { + _initAfterConnect(false); + }); + + }) \ No newline at end of file diff --git a/web/app/views/music_sessions/session_info.html.haml b/web/app/views/music_sessions/session_info.html.haml index 12da1ff4a..3fcbdba8d 100644 --- a/web/app/views/music_sessions/session_info.html.haml +++ b/web/app/views/music_sessions/session_info.html.haml @@ -14,10 +14,10 @@ %span.f12 Session Creator %br/ %br/ - .f12 Tell the session creator you'd like to play in this session - %br/ - %a.button-orange{:id => "btn-rsvp"} - RSVP NOW! + - if current_user.id != @music_session.creator.id + .f12.call-to-action + %br/ + %a.button-orange{:id => "btn-action"} .landing-details .left.f20.teal %strong SESSION diff --git a/web/app/views/music_sessions/show.html.erb b/web/app/views/music_sessions/show.html.erb index d55ab1f11..fd888a1e8 100644 --- a/web/app/views/music_sessions/show.html.erb +++ b/web/app/views/music_sessions/show.html.erb @@ -121,7 +121,7 @@ <% if @music_session.fan_access %> <% content_for :after_black_bar do %>
    - <%= render :partial => "shared/comments", :locals => {:comments => @music_session.comments, :id => "txtSessionComment"} %> + <%= render :partial => "shared/comments", :locals => {:comments => @music_session.comments, :id => "txtSessionComment", :info_page => false} %> <% end %> <% content_for :extra_dialogs do %> diff --git a/web/app/views/recordings/show.html.erb b/web/app/views/recordings/show.html.erb index 52839832d..f9b17ce5b 100644 --- a/web/app/views/recordings/show.html.erb +++ b/web/app/views/recordings/show.html.erb @@ -110,7 +110,7 @@ <% content_for :after_black_bar do %>
    - <%= render :partial => "shared/comments", :locals => {:comments => @claimed_recording.recording.comments, :id => "txtRecordingComment"} %> + <%= render :partial => "shared/comments", :locals => {:comments => @claimed_recording.recording.comments, :id => "txtRecordingComment", :info_page => false} %> <% end %> diff --git a/web/app/views/shared/_ga.html.erb b/web/app/views/shared/_ga.html.erb index e3957a4d5..f22ac3a63 100644 --- a/web/app/views/shared/_ga.html.erb +++ b/web/app/views/shared/_ga.html.erb @@ -1,4 +1,4 @@ -<% if current_user.nil? || !Rails.application.config.ga_suppress_admin || !current_user.admin? # remove admin users from GA %> +<% if request.user_agent != "monitor" && (current_user.nil? || !Rails.application.config.ga_suppress_admin || !current_user.admin?) # remove admin users from GA %>