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/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/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..bb947342b 100644 --- a/admin/spec/factories.rb +++ b/admin/spec/factories.rb @@ -46,4 +46,131 @@ 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 + 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/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..3bb46bcc5 100755 --- a/db/manifest +++ b/db/manifest @@ -161,4 +161,10 @@ 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 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/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..124d798a9 --- 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 } @@ -28,7 +33,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 +50,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 +93,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..5df32957d --- /dev/null +++ b/monitor/spec/spec_helper.rb @@ -0,0 +1,43 @@ +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 +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..722583038 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" diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index ae2c1717e..48f217d47 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -42,6 +42,7 @@ 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/google_analytics_event" require "jam_ruby/mq_router" require "jam_ruby/base_manager" @@ -145,10 +146,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_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/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 150942542..23bdd724c 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 @@ -106,9 +107,9 @@ 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 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..110ddcfac --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_dl_notrun.text.erb @@ -0,0 +1,7 @@ +<% 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. + +-- 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..cb5d3dbaa --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_notdl.text.erb @@ -0,0 +1,11 @@ +<% 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: 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..190ae73ae --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/client_run_notgear.text.erb @@ -0,0 +1,13 @@ +<% 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/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..74978f1e3 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/gear_notsess.text.erb @@ -0,0 +1,22 @@ +<% 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: 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..24f5360b3 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notconnect.text.erb @@ -0,0 +1,20 @@ +<% 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: 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..05d50f493 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notinvite.text.erb @@ -0,0 +1,11 @@ +<% 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_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..86ea83312 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/reg_notlike.text.erb @@ -0,0 +1,12 @@ +<% 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| %> + 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..a0519e8f3 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/sess_notgood.text.erb @@ -0,0 +1,13 @@ +<% 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_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..614cd98fc --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/progress_mailer/sess_notrecord.text.erb @@ -0,0 +1,11 @@ +<% 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: + +- 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..51048fb79 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') %> +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/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..5d4eefd2c 100644 --- a/ruby/lib/jam_ruby/connection_manager.rb +++ b/ruby/lib/jam_ruby/connection_manager.rb @@ -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/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..82236b295 100644 --- a/ruby/lib/jam_ruby/models/connection.rb +++ b/ruby/lib/jam_ruby/models/connection.rb @@ -134,7 +134,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 +148,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) diff --git a/ruby/lib/jam_ruby/models/email_batch.rb b/ruby/lib/jam_ruby/models/email_batch.rb index 02c729dda..a816ac422 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 =< { + :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 =< '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.email_batch_id = batch.id bset.user_ids = user_ids.join(',') bset.started_at = Time.now bset.batch_count = user_ids.size 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 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/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/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/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/score.rb b/ruby/lib/jam_ruby/models/score.rb index 570d409f4..ea25480d7 100644 --- a/ruby/lib/jam_ruby/models/score.rb +++ b/ruby/lib/jam_ruby/models/score.rb @@ -5,13 +5,15 @@ module JamRuby 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 +27,9 @@ 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..b9745984a 100644 --- a/ruby/lib/jam_ruby/models/search.rb +++ b/ruby/lib/jam_ruby/models/search.rb @@ -120,44 +120,95 @@ 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 - score must be <= this to be included in the result + # handled by relation_pagination: + # page - page number to fetch (origin 1) + # per_page - number of entries per page + # handled by order_param: + # orderby - ??? (followed, plays, playing) + # 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 + # todo scott what is the real limit? + score_limit = 60 + l = params[:score_limit] + unless l.nil? or l.to_i <= 0 + score_limit = l.to_i 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? + rel = rel.joins('inner join scores on scores.alocidispid = users.last_jam_locidispid') + .where(['scores.blocidispid = ?', locidispid]) + .where(['scores.score <= ?', score_limit]) + + 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 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("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') + 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]) objs = rel.all + + # puts "======================== objs #{objs.inspect}" + 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 +324,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..0ed32cc68 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -12,9 +12,9 @@ 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 @@ -140,7 +140,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 +221,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? @@ -776,12 +776,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 +1103,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 +1170,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/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..fd8343d22 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -451,6 +451,12 @@ 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 :notification, :class => JamRuby::Notification do factory :notification_text_message 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/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/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/spec_helper.rb b/ruby/spec/spec_helper.rb index c29b007e6..1ab24989e 100644 --- a/ruby/spec/spec_helper.rb +++ b/ruby/spec/spec_helper.rb @@ -30,7 +30,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/web/Gemfile b/web/Gemfile index b024e9cfe..d3237ae91 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" diff --git a/web/app/assets/javascripts/chatPanel.js b/web/app/assets/javascripts/chatPanel.js index fef16ab0b..7b7496a53 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 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/ga.js b/web/app/assets/javascripts/ga.js index a2d30f72b..9b5ea6d24 100644 --- a/web/app/assets/javascripts/ga.js +++ b/web/app/assets/javascripts/ga.js @@ -79,7 +79,10 @@ band : 'Band', fan : 'Fan', recording : 'Recording', - session : 'Session' + session : 'Session', + facebook: 'facebook', + twitter: 'twitter', + google: 'google', }; var categories = { @@ -271,11 +274,11 @@ 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); } diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index d1ffbb7bb..c723ed626 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; diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index bcbb6d295..39521acd0 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -657,7 +657,10 @@ } 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, 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_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..cabcf557a 100644 --- a/web/app/controllers/api_search_controller.rb +++ b/web/app/controllers/api_search_controller.rb @@ -7,15 +7,17 @@ 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) + @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/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/views/clients/_footer.html.erb b/web/app/views/clients/_footer.html.erb index 5c4fee259..4a3ea841a 100644 --- a/web/app/views/clients/_footer.html.erb +++ b/web/app/views/clients/_footer.html.erb @@ -1,6 +1,5 @@
- <% [: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 %>