From 562dd17b361111495d0a07c13c6e22680b3b9aa2 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 10 Sep 2015 17:43:48 +0800 Subject: [PATCH 001/305] update Gemfile for rails 4 --- Gemfile | 26 +---- Gemfile.lock | 266 +++++++++++++++++++++++++++------------------------ 2 files changed, 147 insertions(+), 145 deletions(-) diff --git a/Gemfile b/Gemfile index 9489f21d..8d76353a 100644 --- a/Gemfile +++ b/Gemfile @@ -1,10 +1,7 @@ source 'https://rubygems.org' ruby '2.1.3' -gem 'rails', '3.2.17' - -# Bundle edge Rails instead: -# gem 'rails', :git => 'git://github.com/rails/rails.git' +gem 'rails', '4.2.4' gem 'devise' gem 'redis' @@ -29,32 +26,19 @@ gem 'aws-sdk' # in production environments by default. group :assets do gem 'sass-rails' - gem 'coffee-rails', '~> 3.2.1' + gem 'coffee-rails' # See https://github.com/sstephenson/execjs#readme for more supported runtimes # gem 'therubyracer' - gem 'uglifier', '>= 1.0.3' + gem 'uglifier' end group :production do #this is used on heroku #gem 'rmagick' end -gem 'jquery-rails', '2.1.2' - -# To use ActiveModel has_secure_password -# gem 'bcrypt-ruby', '~> 3.0.0' +gem 'jquery-rails' # To use Jbuilder templates for JSON - gem 'jbuilder', '0.8.2' - -# Use unicorn as the web server -# gem 'unicorn' - -# Deploy with Capistrano -# gem 'capistrano' - -# To use debugger -# gem 'ruby-debug19', :require => 'ruby-debug' - +gem 'jbuilder' diff --git a/Gemfile.lock b/Gemfile.lock index 98a5e233..bcbf74b8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,58 +1,66 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (3.2.17) - actionpack (= 3.2.17) - mail (~> 2.5.4) - actionpack (3.2.17) - activemodel (= 3.2.17) - activesupport (= 3.2.17) - builder (~> 3.0.0) + actionmailer (4.2.4) + actionpack (= 4.2.4) + actionview (= 4.2.4) + activejob (= 4.2.4) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 1.0, >= 1.0.5) + actionpack (4.2.4) + actionview (= 4.2.4) + activesupport (= 4.2.4) + rack (~> 1.6) + rack-test (~> 0.6.2) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (4.2.4) + activesupport (= 4.2.4) + builder (~> 3.1) erubis (~> 2.7.0) - journey (~> 1.0.4) - rack (~> 1.4.5) - rack-cache (~> 1.2) - rack-test (~> 0.6.1) - sprockets (~> 2.2.1) - activemodel (3.2.17) - activesupport (= 3.2.17) - builder (~> 3.0.0) - activerecord (3.2.17) - activemodel (= 3.2.17) - activesupport (= 3.2.17) - arel (~> 3.0.2) - tzinfo (~> 0.3.29) - activeresource (3.2.17) - activemodel (= 3.2.17) - activesupport (= 3.2.17) - activesupport (3.2.17) - i18n (~> 0.6, >= 0.6.4) - multi_json (~> 1.0) - arel (3.0.3) - aws-sdk (1.54.0) - aws-sdk-v1 (= 1.54.0) - aws-sdk-v1 (1.54.0) - json (~> 1.4) - nokogiri (>= 1.4.4) - bcrypt (3.1.7) - bcrypt (3.1.7-x86-mingw32) - best_in_place (2.1.0) - jquery-rails - rails (~> 3.1) - builder (3.0.4) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + activejob (4.2.4) + activesupport (= 4.2.4) + globalid (>= 0.3.0) + activemodel (4.2.4) + activesupport (= 4.2.4) + builder (~> 3.1) + activerecord (4.2.4) + activemodel (= 4.2.4) + activesupport (= 4.2.4) + arel (~> 6.0) + activesupport (4.2.4) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + arel (6.0.3) + aws-sdk (2.1.19) + aws-sdk-resources (= 2.1.19) + aws-sdk-core (2.1.19) + jmespath (~> 1.0) + aws-sdk-resources (2.1.19) + aws-sdk-core (= 2.1.19) + bcrypt (3.1.10) + best_in_place (3.0.3) + actionpack (>= 3.2) + railties (>= 3.2) + builder (3.2.2) cancan (1.6.10) climate_control (0.0.3) activesupport (>= 3.0) - cocaine (0.5.4) + cocaine (0.5.7) climate_control (>= 0.0.3, < 1.0) - coffee-rails (3.2.2) + coffee-rails (4.1.0) coffee-script (>= 2.2.0) - railties (~> 3.2.0) - coffee-script (2.3.0) + railties (>= 4.0.0, < 5.0) + coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.8.0) - devise (3.4.0) + coffee-script-source (1.9.1.1) + devise (3.5.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 3.2.6, < 5) @@ -61,92 +69,100 @@ GEM warden (~> 1.2.3) dotenv (2.0.0) erubis (2.7.0) - execjs (2.2.1) + execjs (2.6.0) ezcrypto (0.7.2) - formtastic (3.0.0) + formtastic (3.1.3) actionpack (>= 3.2.13) - formula (1.0.1) + formula (1.1.1) rails (> 3.0.0) - hike (1.2.3) - i18n (0.6.11) - jbuilder (0.8.2) - activesupport (>= 3.0.0) - journey (1.0.4) - jquery-rails (2.1.2) - railties (>= 3.1.0, < 5.0) - thor (~> 0.14) - json (1.8.1) - kaminari (0.16.1) + globalid (0.3.6) + activesupport (>= 4.1.0) + i18n (0.7.0) + jbuilder (2.3.1) + activesupport (>= 3.0.0, < 5) + multi_json (~> 1.2) + jmespath (1.0.2) + multi_json (~> 1.0) + jquery-rails (4.0.5) + rails-dom-testing (~> 1.0) + railties (>= 4.2.0) + thor (>= 0.14, < 2.0) + json (1.8.3) + kaminari (0.16.3) actionpack (>= 3.0.0) activesupport (>= 3.0.0) - mail (2.5.4) - mime-types (~> 1.16) - treetop (~> 1.4.8) - mime-types (1.25.1) - mini_portile (0.6.0) - multi_json (1.10.1) - nokogiri (1.6.3.1) - mini_portile (= 0.6.0) - nokogiri (1.6.3.1-x86-mingw32) - mini_portile (= 0.6.0) + loofah (2.0.3) + nokogiri (>= 1.5.9) + mail (2.6.3) + mime-types (>= 1.16, < 3) + mime-types (2.6.1) + mimemagic (0.3.0) + mini_portile (0.6.2) + minitest (5.8.0) + multi_json (1.11.2) + nokogiri (1.6.6.2) + mini_portile (~> 0.6.0) oauth (0.4.7) orm_adapter (0.5.0) - paperclip (4.2.0) - activemodel (>= 3.0.0) - activesupport (>= 3.0.0) - cocaine (~> 0.5.3) + paperclip (4.3.0) + activemodel (>= 3.2.0) + activesupport (>= 3.2.0) + cocaine (~> 0.5.5) mime-types - pg (0.17.1) - pg (0.17.1-x86-mingw32) - polyglot (0.3.5) - rack (1.4.5) - rack-cache (1.2) - rack (>= 0.4) - rack-ssl (1.3.4) - rack - rack-test (0.6.2) + mimemagic (= 0.3.0) + pg (0.18.3) + rack (1.6.4) + rack-test (0.6.3) rack (>= 1.0) - rails (3.2.17) - actionmailer (= 3.2.17) - actionpack (= 3.2.17) - activerecord (= 3.2.17) - activeresource (= 3.2.17) - activesupport (= 3.2.17) - bundler (~> 1.0) - railties (= 3.2.17) - rails3-jquery-autocomplete (1.0.14) - rails (>= 3.0) - railties (3.2.17) - actionpack (= 3.2.17) - activesupport (= 3.2.17) - rack-ssl (~> 1.3.2) + rails (4.2.4) + actionmailer (= 4.2.4) + actionpack (= 4.2.4) + actionview (= 4.2.4) + activejob (= 4.2.4) + activemodel (= 4.2.4) + activerecord (= 4.2.4) + activesupport (= 4.2.4) + bundler (>= 1.3.0, < 2.0) + railties (= 4.2.4) + sprockets-rails + rails-deprecated_sanitizer (1.0.3) + activesupport (>= 4.2.0.alpha) + rails-dom-testing (1.0.7) + activesupport (>= 4.2.0.beta, < 5.0) + nokogiri (~> 1.6.0) + rails-deprecated_sanitizer (>= 1.0.1) + rails-html-sanitizer (1.0.2) + loofah (~> 2.0) + rails3-jquery-autocomplete (1.0.15) + rails (>= 3.2) + railties (4.2.4) + actionpack (= 4.2.4) + activesupport (= 4.2.4) rake (>= 0.8.7) - rdoc (~> 3.4) - thor (>= 0.14.6, < 2.0) - rake (10.3.2) - rdoc (3.12.2) - json (~> 1.4) - redis (3.1.0) - responders (1.1.1) - railties (>= 3.2, < 4.2) - sass (3.4.5) - sass-rails (3.2.6) - railties (~> 3.2.0) - sass (>= 3.1.10) - tilt (~> 1.3) - sprockets (2.2.2) - hike (~> 1.2) - multi_json (~> 1.0) + thor (>= 0.18.1, < 2.0) + rake (10.4.2) + redis (3.2.1) + responders (2.1.0) + railties (>= 4.2.0, < 5) + sass (3.4.18) + sass-rails (5.0.4) + railties (>= 4.0.0, < 5.0) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) + sprockets (3.3.4) rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) + sprockets-rails (2.3.3) + actionpack (>= 3.0) + activesupport (>= 3.0) + sprockets (>= 2.8, < 4.0) thor (0.19.1) - thread_safe (0.3.4) - tilt (1.4.1) - treetop (1.4.15) - polyglot - polyglot (>= 0.3.1) - tzinfo (0.3.41) - uglifier (2.5.3) + thread_safe (0.3.5) + tilt (2.0.1) + tzinfo (1.2.2) + thread_safe (~> 0.1) + uglifier (2.7.2) execjs (>= 0.3.0) json (>= 1.8.0) uservoice-ruby (0.0.11) @@ -158,26 +174,28 @@ GEM PLATFORMS ruby - x86-mingw32 DEPENDENCIES aws-sdk best_in_place cancan - coffee-rails (~> 3.2.1) + coffee-rails devise dotenv formtastic formula - jbuilder (= 0.8.2) - jquery-rails (= 2.1.2) + jbuilder + jquery-rails json kaminari paperclip pg - rails (= 3.2.17) + rails (= 4.2.4) rails3-jquery-autocomplete redis sass-rails - uglifier (>= 1.0.3) + uglifier uservoice-ruby + +BUNDLED WITH + 1.10.6 From 62e96d574c3b4d16919f230b493314a7e4030fa9 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 10 Sep 2015 17:43:58 +0800 Subject: [PATCH 002/305] some config changes for rails 4 found on the Internet --- config/application.rb | 7 ------- config/environments/development.rb | 15 ++------------- config/environments/production.rb | 3 +++ config/environments/test.rb | 8 ++------ 4 files changed, 7 insertions(+), 26 deletions(-) diff --git a/config/application.rb b/config/application.rb index d9f5f0f1..37547b27 100644 --- a/config/application.rb +++ b/config/application.rb @@ -44,14 +44,7 @@ module Metamaps # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql - # Enforce whitelist mode for mass assignment. - # This will create an empty whitelist of attributes available for mass-assignment for all models - # in your app. As such, your models will need to explicitly whitelist or blacklist accessible - # parameters by using an attr_accessible or attr_protected declaration. - # config.active_record.whitelist_attributes = true - # Enable the asset pipeline - config.assets.enabled = true config.assets.initialize_on_precompile = false # Version of your assets, change this if you want to expire all your assets diff --git a/config/environments/development.rb b/config/environments/development.rb index 74431de7..251e12ab 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,14 +1,13 @@ Metamaps::Application.configure do # Settings specified here will take precedence over those in config/application.rb + config.eager_load = false + # In the development environment your application's code is reloaded on # every request. This slows down response time but is perfect for development # since you don't have to restart the web server when you make code changes. config.cache_classes = false - # Log error messages when you accidentally call methods on nil. - config.whiny_nils = true - # Show full error reports and disable caching config.consider_all_requests_local = true config.action_controller.perform_caching = false @@ -40,16 +39,6 @@ Metamaps::Application.configure do # Print deprecation notices to the Rails logger config.active_support.deprecation = :log - # Only use best-standards-support built into browsers - config.action_dispatch.best_standards_support = :builtin - - # Raise exception on mass assignment protection for Active Record models - config.active_record.mass_assignment_sanitizer = :strict - - # Log the query plan for queries taking more than this (works - # with SQLite, MySQL, and PostgreSQL) - config.active_record.auto_explain_threshold_in_seconds = 0.5 - # Do not compress assets config.assets.compress = false diff --git a/config/environments/production.rb b/config/environments/production.rb index 2186327f..17773071 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,6 +1,9 @@ Metamaps::Application.configure do # Settings specified here will take precedence over those in config/application.rb + config.eager_load = true + config.assets.js_compressor = :uglifier + # Code is not reloaded between requests config.cache_classes = true diff --git a/config/environments/test.rb b/config/environments/test.rb index 8a7b408c..18c17296 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,6 +1,8 @@ Metamaps::Application.configure do # Settings specified here will take precedence over those in config/application.rb + config.eager_load = false + # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped @@ -11,9 +13,6 @@ Metamaps::Application.configure do config.serve_static_assets = true config.static_cache_control = "public, max-age=3600" - # Log error messages when you accidentally call methods on nil - config.whiny_nils = true - # Show full error reports and disable caching config.consider_all_requests_local = true config.action_controller.perform_caching = false @@ -29,9 +28,6 @@ Metamaps::Application.configure do # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test - # Raise exception on mass assignment protection for Active Record models - config.active_record.mass_assignment_sanitizer = :strict - # Print deprecation notices to the stderr config.active_support.deprecation = :stderr end From 558ced62a86a2aea19ebdefa1cec5cb050c71bb6 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 10 Sep 2015 22:03:39 +0800 Subject: [PATCH 003/305] asset precompiling simpler in rails 4 --- config/application.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/config/application.rb b/config/application.rb index 37547b27..0a5e6b51 100644 --- a/config/application.rb +++ b/config/application.rb @@ -2,12 +2,7 @@ require File.expand_path('../boot', __FILE__) require 'rails/all' -if defined?(Bundler) - # If you precompile assets before deploying to production, use this line - Bundler.require(*Rails.groups(:assets => %w(development test))) - # If you want your assets lazily compiled in production, use this line - # Bundler.require(:default, :assets, Rails.env) -end +Bundler.require(:default, Rails.env) module Metamaps class Application < Rails::Application From 61159dc44c82a36463ca9c138aa795bbd759da3d Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 10 Sep 2015 22:06:58 +0800 Subject: [PATCH 004/305] remove match method from routes.rb --- config/routes.rb | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 494992a6..3c79f881 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,12 +2,12 @@ Metamaps::Application.routes.draw do root to: 'main#home', via: :get - match 'request', to: 'main#requestinvite', via: :get, as: :request + get 'request', to: 'main#requestinvite', as: :request - match 'search/topics', to: 'main#searchtopics', via: :get, as: :searchtopics - match 'search/maps', to: 'main#searchmaps', via: :get, as: :searchmaps - match 'search/mappers', to: 'main#searchmappers', via: :get, as: :searchmappers - match 'search/synapses', to: 'main#searchsynapses', via: :get, as: :searchsynapses + get 'search/topics', to: 'main#searchtopics', as: :searchtopics + get 'search/maps', to: 'main#searchmaps', as: :searchmaps + get 'search/mappers', to: 'main#searchmappers', as: :searchmappers + get 'search/synapses', to: 'main#searchsynapses', as: :searchsynapses resources :mappings, except: [:index, :new, :edit] resources :metacode_sets, :except => [:show] @@ -16,28 +16,28 @@ Metamaps::Application.routes.draw do resources :topics, except: [:index, :new, :edit] do get :autocomplete_topic, :on => :collection end - match 'topics/:id/network', to: 'topics#network', via: :get, as: :network - match 'topics/:id/relative_numbers', to: 'topics#relative_numbers', via: :get, as: :relative_numbers - match 'topics/:id/relatives', to: 'topics#relatives', via: :get, as: :relatives + get 'topics/:id/network', to: 'topics#network', as: :network + get 'topics/:id/relative_numbers', to: 'topics#relative_numbers', as: :relative_numbers + get 'topics/:id/relatives', to: 'topics#relatives', as: :relatives - match 'explore/active', to: 'maps#index', via: :get, as: :activemaps - match 'explore/featured', to: 'maps#index', via: :get, as: :featuredmaps - match 'explore/mine', to: 'maps#index', via: :get, as: :mymaps - match 'explore/mapper/:id', to: 'maps#index', via: :get, as: :usermaps + get 'explore/active', to: 'maps#index', as: :activemaps + get 'explore/featured', to: 'maps#index', as: :featuredmaps + get 'explore/mine', to: 'maps#index', as: :mymaps + get 'explore/mapper/:id', to: 'maps#index', as: :usermaps resources :maps, except: [:new, :edit] - match 'maps/:id/contains', to: 'maps#contains', via: :get, as: :contains - match 'maps/:id/upload_screenshot', to: 'maps#screenshot', via: :post, as: :screenshot + get 'maps/:id/contains', to: 'maps#contains', as: :contains + get 'maps/:id/upload_screenshot', to: 'maps#screenshot', as: :screenshot - devise_for :users, controllers: { registrations: 'users/registrations', passwords: 'users/passwords', sessions: 'devise/sessions' }, :skip => [:sessions] + devise_for :users, controllers: { registrations: 'users/registrations', passwords: 'users/passwords', sessions: 'devise/sessions' }, :skip => :sessions devise_scope :user do get 'login' => 'devise/sessions#new', :as => :new_user_session post 'login' => 'devise/sessions#create', :as => :user_session get 'logout' => 'devise/sessions#destroy', :as => :destroy_user_session - get 'join' => 'devise/registrations#new', :as => :new_user_registration +# get 'join' => 'devise/registrations#new', :as => :new_user_registration end - match 'users/:id/details', to: 'users#details', via: :get, as: :details - match 'user/updatemetacodes', to: 'users#updatemetacodes', via: :post, as: :updatemetacodes + get 'users/:id/details', to: 'users#details', as: :details + post 'user/updatemetacodes', to: 'users#updatemetacodes', as: :updatemetacodes resources :users, except: [:index, :destroy] end From 13b70be65336c2291aae803ce618bfae65eafcef Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 10 Sep 2015 22:12:39 +0800 Subject: [PATCH 005/305] secret_token => secret_key_base --- config/initializers/secret_token.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index b9f46222..9c1fb05e 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -4,4 +4,4 @@ # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -Metamaps::Application.config.secret_token = '267c8a84f63963282f45bc3010eaddf027abfab58fc759d6e239c8005f85ee99d6d01b1ab6394cdee9ca7f8c9213a0cf91d3d8d3350f096123e2caccbcc0924f' +Metamaps::Application.config.secret_key_base = '267c8a84f63963282f45bc3010eaddf027abfab58fc759d6e239c8005f85ee99d6d01b1ab6394cdee9ca7f8c9213a0cf91d3d8d3350f096123e2caccbcc0924f' From 371851cb8921fa93c61826f0d59cd8c487bd4180 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 10 Sep 2015 22:12:50 +0800 Subject: [PATCH 006/305] remove attr_accessible --- app/controllers/metacode_sets_controller.rb | 4 ++++ app/controllers/users_controller.rb | 4 ++++ app/models/in_metacode_set.rb | 1 - app/models/metacode_set.rb | 1 - app/models/user.rb | 2 -- 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/controllers/metacode_sets_controller.rb b/app/controllers/metacode_sets_controller.rb index 4542f6dd..6560492a 100644 --- a/app/controllers/metacode_sets_controller.rb +++ b/app/controllers/metacode_sets_controller.rb @@ -1,6 +1,10 @@ class MetacodeSetsController < ApplicationController before_filter :require_admin + + def metacode_set_params + params.require(:metacode_set).permit(:desc, :mapperContributed, :name) + end # GET /metacode_sets # GET /metacode_sets.json diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 58ef2d96..81ed2122 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -3,6 +3,10 @@ class UsersController < ApplicationController before_filter :require_user, only: [:edit, :update, :updatemetacodes] respond_to :html, :json + + def user_params + params.require(:user).permit(:name, :email, :image, :password, + :password_confirmation, :code, :joinedwithcode, :remember_me) # GET /users/1.json def show diff --git a/app/models/in_metacode_set.rb b/app/models/in_metacode_set.rb index 3f608587..117033d6 100644 --- a/app/models/in_metacode_set.rb +++ b/app/models/in_metacode_set.rb @@ -1,5 +1,4 @@ class InMetacodeSet < ActiveRecord::Base belongs_to :metacode, :class_name => "Metacode", :foreign_key => "metacode_id" belongs_to :metacode_set, :class_name => "MetacodeSet", :foreign_key => "metacode_set_id" - # attr_accessible :title, :body end diff --git a/app/models/metacode_set.rb b/app/models/metacode_set.rb index 6798e568..d06de1e4 100644 --- a/app/models/metacode_set.rb +++ b/app/models/metacode_set.rb @@ -1,6 +1,5 @@ class MetacodeSet < ActiveRecord::Base belongs_to :user - attr_accessible :desc, :mapperContributed, :name has_many :in_metacode_sets has_many :metacodes, :through => :in_metacode_sets end diff --git a/app/models/user.rb b/app/models/user.rb index 2e738134..44bd6b4a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -11,8 +11,6 @@ class User < ActiveRecord::Base devise :database_authenticatable, :recoverable, :rememberable, :trackable, :registerable - attr_accessible :name, :email, :image, :password, :password_confirmation, :code, :joinedwithcode, :remember_me - serialize :settings, UserPreference validates :password, :presence => true, From 7b199983d860de00ba80aa2209464347bb911f2d Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 10 Sep 2015 22:13:10 +0800 Subject: [PATCH 007/305] add jquery_ui to gemfile --- Gemfile | 1 + Gemfile.lock | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index 8d76353a..aef0efeb 100644 --- a/Gemfile +++ b/Gemfile @@ -39,6 +39,7 @@ group :production do #this is used on heroku end gem 'jquery-rails' +gem 'jquery-ui-rails' # To use Jbuilder templates for JSON gem 'jbuilder' diff --git a/Gemfile.lock b/Gemfile.lock index bcbf74b8..8db5830f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -87,6 +87,8 @@ GEM rails-dom-testing (~> 1.0) railties (>= 4.2.0) thor (>= 0.14, < 2.0) + jquery-ui-rails (5.0.5) + railties (>= 3.2.16) json (1.8.3) kaminari (0.16.3) actionpack (>= 3.0.0) @@ -186,6 +188,7 @@ DEPENDENCIES formula jbuilder jquery-rails + jquery-ui-rails json kaminari paperclip From 8bf2eb31f37fb0dac90f835def94dc90583d9dfc Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 10 Sep 2015 22:40:45 +0800 Subject: [PATCH 008/305] remove old jquery-ui file --- .../lib/jquery-ui-1.8.23.custom.min.js | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 app/assets/javascripts/lib/jquery-ui-1.8.23.custom.min.js diff --git a/app/assets/javascripts/lib/jquery-ui-1.8.23.custom.min.js b/app/assets/javascripts/lib/jquery-ui-1.8.23.custom.min.js deleted file mode 100644 index 564759cd..00000000 --- a/app/assets/javascripts/lib/jquery-ui-1.8.23.custom.min.js +++ /dev/null @@ -1,25 +0,0 @@ -/*! jQuery UI - v1.8.23 - 2012-08-15 -* https://github.com/jquery/jquery-ui -* Includes: jquery.ui.core.js -* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ -(function(a,b){function c(b,c){var e=b.nodeName.toLowerCase();if("area"===e){var f=b.parentNode,g=f.name,h;return!b.href||!g||f.nodeName.toLowerCase()!=="map"?!1:(h=a("img[usemap=#"+g+"]")[0],!!h&&d(h))}return(/input|select|textarea|button|object/.test(e)?!b.disabled:"a"==e?b.href||c:c)&&d(b)}function d(b){return!a(b).parents().andSelf().filter(function(){return a.curCSS(this,"visibility")==="hidden"||a.expr.filters.hidden(this)}).length}a.ui=a.ui||{};if(a.ui.version)return;a.extend(a.ui,{version:"1.8.23",keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}}),a.fn.extend({propAttr:a.fn.prop||a.fn.attr,_focus:a.fn.focus,focus:function(b,c){return typeof b=="number"?this.each(function(){var d=this;setTimeout(function(){a(d).focus(),c&&c.call(d)},b)}):this._focus.apply(this,arguments)},scrollParent:function(){var b;return a.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?b=this.parents().filter(function(){return/(relative|absolute|fixed)/.test(a.curCSS(this,"position",1))&&/(auto|scroll)/.test(a.curCSS(this,"overflow",1)+a.curCSS(this,"overflow-y",1)+a.curCSS(this,"overflow-x",1))}).eq(0):b=this.parents().filter(function(){return/(auto|scroll)/.test(a.curCSS(this,"overflow",1)+a.curCSS(this,"overflow-y",1)+a.curCSS(this,"overflow-x",1))}).eq(0),/fixed/.test(this.css("position"))||!b.length?a(document):b},zIndex:function(c){if(c!==b)return this.css("zIndex",c);if(this.length){var d=a(this[0]),e,f;while(d.length&&d[0]!==document){e=d.css("position");if(e==="absolute"||e==="relative"||e==="fixed"){f=parseInt(d.css("zIndex"),10);if(!isNaN(f)&&f!==0)return f}d=d.parent()}}return 0},disableSelection:function(){return this.bind((a.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),a("").outerWidth(1).jquery||a.each(["Width","Height"],function(c,d){function h(b,c,d,f){return a.each(e,function(){c-=parseFloat(a.curCSS(b,"padding"+this,!0))||0,d&&(c-=parseFloat(a.curCSS(b,"border"+this+"Width",!0))||0),f&&(c-=parseFloat(a.curCSS(b,"margin"+this,!0))||0)}),c}var e=d==="Width"?["Left","Right"]:["Top","Bottom"],f=d.toLowerCase(),g={innerWidth:a.fn.innerWidth,innerHeight:a.fn.innerHeight,outerWidth:a.fn.outerWidth,outerHeight:a.fn.outerHeight};a.fn["inner"+d]=function(c){return c===b?g["inner"+d].call(this):this.each(function(){a(this).css(f,h(this,c)+"px")})},a.fn["outer"+d]=function(b,c){return typeof b!="number"?g["outer"+d].call(this,b):this.each(function(){a(this).css(f,h(this,b,!0,c)+"px")})}}),a.extend(a.expr[":"],{data:a.expr.createPseudo?a.expr.createPseudo(function(b){return function(c){return!!a.data(c,b)}}):function(b,c,d){return!!a.data(b,d[3])},focusable:function(b){return c(b,!isNaN(a.attr(b,"tabindex")))},tabbable:function(b){var d=a.attr(b,"tabindex"),e=isNaN(d);return(e||d>=0)&&c(b,!e)}}),a(function(){var b=document.body,c=b.appendChild(c=document.createElement("div"));c.offsetHeight,a.extend(c.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0}),a.support.minHeight=c.offsetHeight===100,a.support.selectstart="onselectstart"in c,b.removeChild(c).style.display="none"}),a.curCSS||(a.curCSS=a.css),a.extend(a.ui,{plugin:{add:function(b,c,d){var e=a.ui[b].prototype;for(var f in d)e.plugins[f]=e.plugins[f]||[],e.plugins[f].push([c,d[f]])},call:function(a,b,c){var d=a.plugins[b];if(!d||!a.element[0].parentNode)return;for(var e=0;e0?!0:(b[d]=1,e=b[d]>0,b[d]=0,e)},isOverAxis:function(a,b,c){return a>b&&a=9||!!b.button?this._mouseStarted?(this._mouseDrag(b),b.preventDefault()):(this._mouseDistanceMet(b)&&this._mouseDelayMet(b)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,b)!==!1,this._mouseStarted?this._mouseDrag(b):this._mouseUp(b)),!this._mouseStarted):this._mouseUp(b)},_mouseUp:function(b){return a(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,b.target==this._mouseDownEvent.target&&a.data(b.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(b)),!1},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(a){return this.mouseDelayMet},_mouseStart:function(a){},_mouseDrag:function(a){},_mouseStop:function(a){},_mouseCapture:function(a){return!0}})})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15 -* https://github.com/jquery/jquery-ui -* Includes: jquery.ui.position.js -* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ -(function(a,b){a.ui=a.ui||{};var c=/left|center|right/,d=/top|center|bottom/,e="center",f={},g=a.fn.position,h=a.fn.offset;a.fn.position=function(b){if(!b||!b.of)return g.apply(this,arguments);b=a.extend({},b);var h=a(b.of),i=h[0],j=(b.collision||"flip").split(" "),k=b.offset?b.offset.split(" "):[0,0],l,m,n;return i.nodeType===9?(l=h.width(),m=h.height(),n={top:0,left:0}):i.setTimeout?(l=h.width(),m=h.height(),n={top:h.scrollTop(),left:h.scrollLeft()}):i.preventDefault?(b.at="left top",l=m=0,n={top:b.of.pageY,left:b.of.pageX}):(l=h.outerWidth(),m=h.outerHeight(),n=h.offset()),a.each(["my","at"],function(){var a=(b[this]||"").split(" ");a.length===1&&(a=c.test(a[0])?a.concat([e]):d.test(a[0])?[e].concat(a):[e,e]),a[0]=c.test(a[0])?a[0]:e,a[1]=d.test(a[1])?a[1]:e,b[this]=a}),j.length===1&&(j[1]=j[0]),k[0]=parseInt(k[0],10)||0,k.length===1&&(k[1]=k[0]),k[1]=parseInt(k[1],10)||0,b.at[0]==="right"?n.left+=l:b.at[0]===e&&(n.left+=l/2),b.at[1]==="bottom"?n.top+=m:b.at[1]===e&&(n.top+=m/2),n.left+=k[0],n.top+=k[1],this.each(function(){var c=a(this),d=c.outerWidth(),g=c.outerHeight(),h=parseInt(a.curCSS(this,"marginLeft",!0))||0,i=parseInt(a.curCSS(this,"marginTop",!0))||0,o=d+h+(parseInt(a.curCSS(this,"marginRight",!0))||0),p=g+i+(parseInt(a.curCSS(this,"marginBottom",!0))||0),q=a.extend({},n),r;b.my[0]==="right"?q.left-=d:b.my[0]===e&&(q.left-=d/2),b.my[1]==="bottom"?q.top-=g:b.my[1]===e&&(q.top-=g/2),f.fractions||(q.left=Math.round(q.left),q.top=Math.round(q.top)),r={left:q.left-h,top:q.top-i},a.each(["left","top"],function(c,e){a.ui.position[j[c]]&&a.ui.position[j[c]][e](q,{targetWidth:l,targetHeight:m,elemWidth:d,elemHeight:g,collisionPosition:r,collisionWidth:o,collisionHeight:p,offset:k,my:b.my,at:b.at})}),a.fn.bgiframe&&c.bgiframe(),c.offset(a.extend(q,{using:b.using}))})},a.ui.position={fit:{left:function(b,c){var d=a(window),e=c.collisionPosition.left+c.collisionWidth-d.width()-d.scrollLeft();b.left=e>0?b.left-e:Math.max(b.left-c.collisionPosition.left,b.left)},top:function(b,c){var d=a(window),e=c.collisionPosition.top+c.collisionHeight-d.height()-d.scrollTop();b.top=e>0?b.top-e:Math.max(b.top-c.collisionPosition.top,b.top)}},flip:{left:function(b,c){if(c.at[0]===e)return;var d=a(window),f=c.collisionPosition.left+c.collisionWidth-d.width()-d.scrollLeft(),g=c.my[0]==="left"?-c.elemWidth:c.my[0]==="right"?c.elemWidth:0,h=c.at[0]==="left"?c.targetWidth:-c.targetWidth,i=-2*c.offset[0];b.left+=c.collisionPosition.left<0?g+h+i:f>0?g+h+i:0},top:function(b,c){if(c.at[1]===e)return;var d=a(window),f=c.collisionPosition.top+c.collisionHeight-d.height()-d.scrollTop(),g=c.my[1]==="top"?-c.elemHeight:c.my[1]==="bottom"?c.elemHeight:0,h=c.at[1]==="top"?c.targetHeight:-c.targetHeight,i=-2*c.offset[1];b.top+=c.collisionPosition.top<0?g+h+i:f>0?g+h+i:0}}},a.offset.setOffset||(a.offset.setOffset=function(b,c){/static/.test(a.curCSS(b,"position"))&&(b.style.position="relative");var d=a(b),e=d.offset(),f=parseInt(a.curCSS(b,"top",!0),10)||0,g=parseInt(a.curCSS(b,"left",!0),10)||0,h={top:c.top-e.top+f,left:c.left-e.left+g};"using"in c?c.using.call(b,h):d.css(h)},a.fn.offset=function(b){var c=this[0];return!c||!c.ownerDocument?null:b?a.isFunction(b)?this.each(function(c){a(this).offset(b.call(this,c,a(this).offset()))}):this.each(function(){a.offset.setOffset(this,b)}):h.call(this)}),a.curCSS||(a.curCSS=a.css),function(){var b=document.getElementsByTagName("body")[0],c=document.createElement("div"),d,e,g,h,i;d=document.createElement(b?"div":"body"),g={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},b&&a.extend(g,{position:"absolute",left:"-1000px",top:"-1000px"});for(var j in g)d.style[j]=g[j];d.appendChild(c),e=b||document.documentElement,e.insertBefore(d,e.firstChild),c.style.cssText="position: absolute; left: 10.7432222px; top: 10.432325px; height: 30px; width: 201px;",h=a(c).offset(function(a,b){return b}).offset(),d.innerHTML="",e.removeChild(d),i=h.top+h.left+(b?2e3:0),f.fractions=i>21&&i<22}()})(jQuery);;/*! jQuery UI - v1.8.23 - 2012-08-15 -* https://github.com/jquery/jquery-ui -* Includes: jquery.ui.draggable.js -* Copyright (c) 2012 AUTHORS.txt; Licensed MIT, GPL */ -(function(a,b){a.widget("ui.draggable",a.ui.mouse,{widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1},_create:function(){this.options.helper=="original"&&!/^(?:r|a|f)/.test(this.element.css("position"))&&(this.element[0].style.position="relative"),this.options.addClasses&&this.element.addClass("ui-draggable"),this.options.disabled&&this.element.addClass("ui-draggable-disabled"),this._mouseInit()},destroy:function(){if(!this.element.data("draggable"))return;return this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled"),this._mouseDestroy(),this},_mouseCapture:function(b){var c=this.options;return this.helper||c.disabled||a(b.target).is(".ui-resizable-handle")?!1:(this.handle=this._getHandle(b),this.handle?(c.iframeFix&&a(c.iframeFix===!0?"iframe":c.iframeFix).each(function(){a('
').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1e3}).css(a(this).offset()).appendTo("body")}),!0):!1)},_mouseStart:function(b){var c=this.options;return this.helper=this._createHelper(b),this.helper.addClass("ui-draggable-dragging"),this._cacheHelperProportions(),a.ui.ddmanager&&(a.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(),this.offset=this.positionAbs=this.element.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},a.extend(this.offset,{click:{left:b.pageX-this.offset.left,top:b.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.originalPosition=this.position=this._generatePosition(b),this.originalPageX=b.pageX,this.originalPageY=b.pageY,c.cursorAt&&this._adjustOffsetFromHelper(c.cursorAt),c.containment&&this._setContainment(),this._trigger("start",b)===!1?(this._clear(),!1):(this._cacheHelperProportions(),a.ui.ddmanager&&!c.dropBehaviour&&a.ui.ddmanager.prepareOffsets(this,b),this._mouseDrag(b,!0),a.ui.ddmanager&&a.ui.ddmanager.dragStart(this,b),!0)},_mouseDrag:function(b,c){this.position=this._generatePosition(b),this.positionAbs=this._convertPositionTo("absolute");if(!c){var d=this._uiHash();if(this._trigger("drag",b,d)===!1)return this._mouseUp({}),!1;this.position=d.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis||this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";return a.ui.ddmanager&&a.ui.ddmanager.drag(this,b),!1},_mouseStop:function(b){var c=!1;a.ui.ddmanager&&!this.options.dropBehaviour&&(c=a.ui.ddmanager.drop(this,b)),this.dropped&&(c=this.dropped,this.dropped=!1);var d=this.element[0],e=!1;while(d&&(d=d.parentNode))d==document&&(e=!0);if(!e&&this.options.helper==="original")return!1;if(this.options.revert=="invalid"&&!c||this.options.revert=="valid"&&c||this.options.revert===!0||a.isFunction(this.options.revert)&&this.options.revert.call(this.element,c)){var f=this;a(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){f._trigger("stop",b)!==!1&&f._clear()})}else this._trigger("stop",b)!==!1&&this._clear();return!1},_mouseUp:function(b){return this.options.iframeFix===!0&&a("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)}),a.ui.ddmanager&&a.ui.ddmanager.dragStop(this,b),a.ui.mouse.prototype._mouseUp.call(this,b)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear(),this},_getHandle:function(b){var c=!this.options.handle||!a(this.options.handle,this.element).length?!0:!1;return a(this.options.handle,this.element).find("*").andSelf().each(function(){this==b.target&&(c=!0)}),c},_createHelper:function(b){var c=this.options,d=a.isFunction(c.helper)?a(c.helper.apply(this.element[0],[b])):c.helper=="clone"?this.element.clone().removeAttr("id"):this.element;return d.parents("body").length||d.appendTo(c.appendTo=="parent"?this.element[0].parentNode:c.appendTo),d[0]!=this.element[0]&&!/(fixed|absolute)/.test(d.css("position"))&&d.css("position","absolute"),d},_adjustOffsetFromHelper:function(b){typeof b=="string"&&(b=b.split(" ")),a.isArray(b)&&(b={left:+b[0],top:+b[1]||0}),"left"in b&&(this.offset.click.left=b.left+this.margins.left),"right"in b&&(this.offset.click.left=this.helperProportions.width-b.right+this.margins.left),"top"in b&&(this.offset.click.top=b.top+this.margins.top),"bottom"in b&&(this.offset.click.top=this.helperProportions.height-b.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var b=this.offsetParent.offset();this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&a.ui.contains(this.scrollParent[0],this.offsetParent[0])&&(b.left+=this.scrollParent.scrollLeft(),b.top+=this.scrollParent.scrollTop());if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&a.browser.msie)b={top:0,left:0};return{top:b.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:b.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var b=this.options;b.containment=="parent"&&(b.containment=this.helper[0].parentNode);if(b.containment=="document"||b.containment=="window")this.containment=[b.containment=="document"?0:a(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,b.containment=="document"?0:a(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,(b.containment=="document"?0:a(window).scrollLeft())+a(b.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(b.containment=="document"?0:a(window).scrollTop())+(a(b.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(b.containment)&&b.containment.constructor!=Array){var c=a(b.containment),d=c[0];if(!d)return;var e=c.offset(),f=a(d).css("overflow")!="hidden";this.containment=[(parseInt(a(d).css("borderLeftWidth"),10)||0)+(parseInt(a(d).css("paddingLeft"),10)||0),(parseInt(a(d).css("borderTopWidth"),10)||0)+(parseInt(a(d).css("paddingTop"),10)||0),(f?Math.max(d.scrollWidth,d.offsetWidth):d.offsetWidth)-(parseInt(a(d).css("borderLeftWidth"),10)||0)-(parseInt(a(d).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(f?Math.max(d.scrollHeight,d.offsetHeight):d.offsetHeight)-(parseInt(a(d).css("borderTopWidth"),10)||0)-(parseInt(a(d).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relative_container=c}else b.containment.constructor==Array&&(this.containment=b.containment)},_convertPositionTo:function(b,c){c||(c=this.position);var d=b=="absolute"?1:-1,e=this.options,f=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,g=/(html|body)/i.test(f[0].tagName);return{top:c.top+this.offset.relative.top*d+this.offset.parent.top*d-(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop():g?0:f.scrollTop())*d),left:c.left+this.offset.relative.left*d+this.offset.parent.left*d-(a.browser.safari&&a.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():g?0:f.scrollLeft())*d)}},_generatePosition:function(b){var c=this.options,d=this.cssPosition=="absolute"&&(this.scrollParent[0]==document||!a.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,e=/(html|body)/i.test(d[0].tagName),f=b.pageX,g=b.pageY;if(this.originalPosition){var h;if(this.containment){if(this.relative_container){var i=this.relative_container.offset();h=[this.containment[0]+i.left,this.containment[1]+i.top,this.containment[2]+i.left,this.containment[3]+i.top]}else h=this.containment;b.pageX-this.offset.click.lefth[2]&&(f=h[2]+this.offset.click.left),b.pageY-this.offset.click.top>h[3]&&(g=h[3]+this.offset.click.top)}if(c.grid){var j=c.grid[1]?this.originalPageY+Math.round((g-this.originalPageY)/c.grid[1])*c.grid[1]:this.originalPageY;g=h?j-this.offset.click.toph[3]?j-this.offset.click.toph[2]?k-this.offset.click.left=0;k--){var l=d.snapElements[k].left,m=l+d.snapElements[k].width,n=d.snapElements[k].top,o=n+d.snapElements[k].height;if(!(l-f").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),e=document.activeElement;try{e.id}catch(f){e=document.body}return b.wrap(d),(b[0]===e||a.contains(b[0],e))&&a(e).focus(),d=b.parent(),b.css("position")=="static"?(d.css({position:"relative"}),b.css({position:"relative"})):(a.extend(c,{position:b.css("position"),zIndex:b.css("z-index")}),a.each(["top","left","bottom","right"],function(a,d){c[d]=b.css(d),isNaN(parseInt(c[d],10))&&(c[d]="auto")}),b.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),d.css(c).show()},removeWrapper:function(b){var c,d=document.activeElement;return b.parent().is(".ui-effects-wrapper")?(c=b.parent().replaceWith(b),(b[0]===d||a.contains(b[0],d))&&a(d).focus(),c):b},setTransition:function(b,c,d,e){return e=e||{},a.each(c,function(a,c){var f=b.cssUnit(c);f[0]>0&&(e[c]=f[0]*d+f[1])}),e}}),a.fn.extend({effect:function(b,c,d,e){var f=k.apply(this,arguments),g={options:f[1],duration:f[2],callback:f[3]},h=g.options.mode,i=a.effects[b];return a.fx.off||!i?h?this[h](g.duration,g.callback):this.each(function(){g.callback&&g.callback.call(this)}):i.call(this,g)},_show:a.fn.show,show:function(a){if(l(a))return this._show.apply(this,arguments);var b=k.apply(this,arguments);return b[1].mode="show",this.effect.apply(this,b)},_hide:a.fn.hide,hide:function(a){if(l(a))return this._hide.apply(this,arguments);var b=k.apply(this,arguments);return b[1].mode="hide",this.effect.apply(this,b)},__toggle:a.fn.toggle,toggle:function(b){if(l(b)||typeof b=="boolean"||a.isFunction(b))return this.__toggle.apply(this,arguments);var c=k.apply(this,arguments);return c[1].mode="toggle",this.effect.apply(this,c)},cssUnit:function(b){var c=this.css(b),d=[];return a.each(["em","px","%","pt"],function(a,b){c.indexOf(b)>0&&(d=[parseFloat(c),b])}),d}});var m={};a.each(["Quad","Cubic","Quart","Quint","Expo"],function(a,b){m[b]=function(b){return Math.pow(b,a+2)}}),a.extend(m,{Sine:function(a){return 1-Math.cos(a*Math.PI/2)},Circ:function(a){return 1-Math.sqrt(1-a*a)},Elastic:function(a){return a===0||a===1?a:-Math.pow(2,8*(a-1))*Math.sin(((a-1)*80-7.5)*Math.PI/15)},Back:function(a){return a*a*(3*a-2)},Bounce:function(a){var b,c=4;while(a<((b=Math.pow(2,--c))-1)/11);return 1/Math.pow(4,3-c)-7.5625*Math.pow((b*3-2)/22-a,2)}}),a.each(m,function(b,c){a.easing["easeIn"+b]=c,a.easing["easeOut"+b]=function(a){return 1-c(1-a)},a.easing["easeInOut"+b]=function(a){return a<.5?c(a*2)/2:c(a*-2+2)/-2+1}})}(jQuery);; \ No newline at end of file From 32311e3610239fb2a0c54d885fe84f52bae3d2fd Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 10 Sep 2015 22:48:35 +0800 Subject: [PATCH 009/305] fix has_many relationships in map & mapping models for rails 4 --- app/models/map.rb | 7 ++----- app/models/mapping.rb | 3 +++ 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/models/map.rb b/app/models/map.rb index 3bf5a4a6..dffb9fae 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -2,11 +2,8 @@ class Map < ActiveRecord::Base belongs_to :user - has_many :topicmappings, :class_name => 'Mapping', :conditions => {:category => 'Topic'} - has_many :synapsemappings, :class_name => 'Mapping', :conditions => {:category => 'Synapse'} - - has_many :topics, :through => :topicmappings - has_many :synapses, :through => :synapsemappings + has_many :topics, -> { Mapping.topicmapping }, :through => :topicmappings + has_many :synapses, -> { Mapping.synapsemapping }, :through => :synapsemappings # This method associates the attribute ":image" with a file attachment has_attached_file :screenshot, :styles => { diff --git a/app/models/mapping.rb b/app/models/mapping.rb index dc50730c..9d6b4947 100644 --- a/app/models/mapping.rb +++ b/app/models/mapping.rb @@ -1,5 +1,8 @@ class Mapping < ActiveRecord::Base + scope :topicmapping, -> { where (category: :Topic) } + scope :synapsemapping, -> { where (category: :Synapse) } + belongs_to :topic, :class_name => "Topic", :foreign_key => "topic_id" belongs_to :synapse, :class_name => "Synapse", :foreign_key => "synapse_id" belongs_to :map, :class_name => "Map", :foreign_key => "map_id" From e9cb8561fac4812677c774170fffc2bd9edcbb2e Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 19 Sep 2015 16:26:34 +0800 Subject: [PATCH 010/305] add required params to all controllers --- app/controllers/mappings_controller.rb | 6 ++++++ app/controllers/maps_controller.rb | 7 +++++++ app/controllers/metacode_sets_controller.rb | 11 +++++++---- app/controllers/metacodes_controller.rb | 7 +++++++ app/controllers/registrations_controller.rb | 2 +- app/controllers/synapses_controller.rb | 6 ++++++ app/controllers/topics_controller.rb | 7 ++++++- app/controllers/users/passwords_controller.rb | 2 +- app/controllers/users_controller.rb | 10 ++++++---- 9 files changed, 47 insertions(+), 11 deletions(-) diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index b28c7638..4898ceb4 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -48,4 +48,10 @@ class MappingsController < ApplicationController head :no_content end + + private + # Never trust parameters from the scary internet, only allow the white list through. + def mapping_params + params.require(:mapping).permit(:id, :category, :xloc, :yloc, :topic_id, :synapse_id, :map_id, :user_id) + end end diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 8f0ced9b..d37be5a3 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -238,4 +238,11 @@ class MapsController < ApplicationController } end end + + private + + # Never trust parameters from the scary internet, only allow the white list through. + def map_params + params.require(:map).permit(:id, :name, :arranged, :desc, :permission, :user_id) + end end diff --git a/app/controllers/metacode_sets_controller.rb b/app/controllers/metacode_sets_controller.rb index 6560492a..376babe5 100644 --- a/app/controllers/metacode_sets_controller.rb +++ b/app/controllers/metacode_sets_controller.rb @@ -2,10 +2,6 @@ class MetacodeSetsController < ApplicationController before_filter :require_admin - def metacode_set_params - params.require(:metacode_set).permit(:desc, :mapperContributed, :name) - end - # GET /metacode_sets # GET /metacode_sets.json def index @@ -120,4 +116,11 @@ class MetacodeSetsController < ApplicationController format.json { head :no_content } end end + + private + + def metacode_set_params + params.require(:metacode_set).permit(:desc, :mapperContributed, :name) + end + end diff --git a/app/controllers/metacodes_controller.rb b/app/controllers/metacodes_controller.rb index 810981dc..25a7c096 100644 --- a/app/controllers/metacodes_controller.rb +++ b/app/controllers/metacodes_controller.rb @@ -93,4 +93,11 @@ class MetacodesController < ApplicationController # format.json { head :no_content } # end # end + + private + + # Never trust parameters from the scary internet, only allow the white list through. + def metacode_params + params.require(:metacode).permit(:id, :name, :icon, :color) + end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 0f0be9ba..5fff2f1c 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -7,4 +7,4 @@ class Users::RegistrationsController < Devise::RegistrationsController def after_update_path_for(resource) signed_in_root_path(resource) end -end \ No newline at end of file +end diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index 6ff1537b..40941960 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -64,4 +64,10 @@ class SynapsesController < ApplicationController format.json { head :no_content } end end + + private + + def synapse_params + params.require(:synapse).permit(:id, :desc, :category, :weight, :permission, :node1_id, :node2_id, :user_id) + end end diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index ca24d1fb..7929c5ac 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -10,7 +10,6 @@ class TopicsController < ApplicationController @current = current_user term = params[:term] if term && !term.empty? - # !connor term here needs to have .downcase @topics = Topic.where('LOWER("name") like ?', term.downcase + '%').order('"name"') #read this next line as 'delete a topic if its private and you're either @@ -233,4 +232,10 @@ class TopicsController < ApplicationController format.json { head :no_content } end end + + private + + def topic_params + params.require(:topic).permit(:id, :name, :desc, :link, :permission, :user_id, :metacode_id) + end end diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb index d1405f6a..ae5517e8 100644 --- a/app/controllers/users/passwords_controller.rb +++ b/app/controllers/users/passwords_controller.rb @@ -3,4 +3,4 @@ class Users::PasswordsController < Devise::PasswordsController def after_resetting_password_path_for(resource) signed_in_root_path(resource) end -end \ No newline at end of file +end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 81ed2122..0141e52d 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -4,10 +4,6 @@ class UsersController < ApplicationController respond_to :html, :json - def user_params - params.require(:user).permit(:name, :email, :image, :password, - :password_confirmation, :code, :joinedwithcode, :remember_me) - # GET /users/1.json def show @user = User.find(params[:id]) @@ -102,4 +98,10 @@ class UsersController < ApplicationController end end + private + + def user_params + params.require(:user).permit(:name, :email, :image, :password, + :password_confirmation, :code, :joinedwithcode, :remember_me) + end From 7d738b7abf206a323a4972b8f2704f1b8d9be480 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 19 Sep 2015 16:48:24 +0800 Subject: [PATCH 011/305] fix map/mapping associations that I broke --- app/controllers/application_controller.rb | 2 +- app/models/map.rb | 6 ++++-- app/models/mapping.rb | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 511ab723..0a3d12ab 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -37,7 +37,7 @@ private end def require_admin - unless authenticated? && user.admin + unless authenticated? && admin? redirect_to root_url, notice: "You need to be an admin for that." return false end diff --git a/app/models/map.rb b/app/models/map.rb index dffb9fae..ee949676 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -2,8 +2,10 @@ class Map < ActiveRecord::Base belongs_to :user - has_many :topics, -> { Mapping.topicmapping }, :through => :topicmappings - has_many :synapses, -> { Mapping.synapsemapping }, :through => :synapsemappings + has_many :topicmappings, -> { Mapping.topicmapping }, class_name: :Mapping + has_many :synapsemappings, -> { Mapping.synapsemapping }, class_name: :Mapping + has_many :topics, through: :topicmappings + has_many :synapses, through: :synapsemappings # This method associates the attribute ":image" with a file attachment has_attached_file :screenshot, :styles => { diff --git a/app/models/mapping.rb b/app/models/mapping.rb index 9d6b4947..a8613840 100644 --- a/app/models/mapping.rb +++ b/app/models/mapping.rb @@ -1,7 +1,7 @@ class Mapping < ActiveRecord::Base - scope :topicmapping, -> { where (category: :Topic) } - scope :synapsemapping, -> { where (category: :Synapse) } + scope :topicmapping, -> { where(category: :Topic) } + scope :synapsemapping, -> { where(category: :Synapse) } belongs_to :topic, :class_name => "Topic", :foreign_key => "topic_id" belongs_to :synapse, :class_name => "Synapse", :foreign_key => "synapse_id" From d4f72bac651d0995cc18757e5991b540c916e783 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 19 Sep 2015 17:01:27 +0800 Subject: [PATCH 012/305] whoo new gems for development. binding.pry is so cool --- Gemfile | 6 ++++++ Gemfile.lock | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/Gemfile b/Gemfile index aef0efeb..5eb0d839 100644 --- a/Gemfile +++ b/Gemfile @@ -38,6 +38,12 @@ group :production do #this is used on heroku #gem 'rmagick' end +group :development, :test do + gem 'pry-rails' + gem 'better_errors' + gem 'quiet_assets' +end + gem 'jquery-rails' gem 'jquery-ui-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 8db5830f..62c0a085 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -47,12 +47,17 @@ GEM best_in_place (3.0.3) actionpack (>= 3.2) railties (>= 3.2) + better_errors (2.1.1) + coderay (>= 1.0.0) + erubis (>= 2.6.6) + rack (>= 0.9.0) builder (3.2.2) cancan (1.6.10) climate_control (0.0.3) activesupport (>= 3.0) cocaine (0.5.7) climate_control (>= 0.0.3, < 1.0) + coderay (1.1.0) coffee-rails (4.1.0) coffee-script (>= 2.2.0) railties (>= 4.0.0, < 5.0) @@ -97,6 +102,7 @@ GEM nokogiri (>= 1.5.9) mail (2.6.3) mime-types (>= 1.16, < 3) + method_source (0.8.2) mime-types (2.6.1) mimemagic (0.3.0) mini_portile (0.6.2) @@ -113,6 +119,14 @@ GEM mime-types mimemagic (= 0.3.0) pg (0.18.3) + pry (0.10.1) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + pry-rails (0.3.4) + pry (>= 0.9.10) + quiet_assets (1.1.0) + railties (>= 3.1, < 5.0) rack (1.6.4) rack-test (0.6.3) rack (>= 1.0) @@ -153,6 +167,7 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) + slop (3.6.0) sprockets (3.3.4) rack (~> 1.0) sprockets-rails (2.3.3) @@ -180,6 +195,7 @@ PLATFORMS DEPENDENCIES aws-sdk best_in_place + better_errors cancan coffee-rails devise @@ -193,6 +209,8 @@ DEPENDENCIES kaminari paperclip pg + pry-rails + quiet_assets rails (= 4.2.4) rails3-jquery-autocomplete redis From 7877e5bdbc6cb3faeaf321412cf18476d20efcb1 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 19 Sep 2015 17:08:12 +0800 Subject: [PATCH 013/305] delete_if threw errors, so convert things to arrays for it --- app/controllers/maps_controller.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index d37be5a3..fd2e32b7 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -72,9 +72,9 @@ class MapsController < ApplicationController respond_to do |format| format.html { @allmappers = @map.contributors - @alltopics = @map.topics.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } - @allsynapses = @map.synapses.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } - @allmappings = @map.mappings.delete_if {|m| + @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } + @allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } + @allmappings = @map.mappings.to_a.delete_if {|m| if m.category == "Synapse" object = m.synapse elsif m.category == "Topic" @@ -100,9 +100,9 @@ class MapsController < ApplicationController end @allmappers = @map.contributors - @alltopics = @map.topics.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } - @allsynapses = @map.synapses.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } - @allmappings = @map.mappings.delete_if {|m| + @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } + @allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } + @allmappings = @map.mappings.to_a.delete_if {|m| if m.category == "Synapse" object = m.synapse elsif m.category == "Topic" From e34b5bd2ad314a1883c48b27196b95195739d5d5 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 19 Sep 2015 17:11:48 +0800 Subject: [PATCH 014/305] fix best in place deprecation warnings in map info box --- app/views/maps/_mapinfobox.html.erb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/maps/_mapinfobox.html.erb b/app/views/maps/_mapinfobox.html.erb index 4305b3a7..134cf8ea 100644 --- a/app/views/maps/_mapinfobox.html.erb +++ b/app/views/maps/_mapinfobox.html.erb @@ -8,7 +8,7 @@ <%= @map && @map.permission != 'private' ? " shareable" : "" %>"> <% if @map %> -
<%= best_in_place @map, :name, :type => :textarea, :activator => "#mapInfoName", :classes => 'best_in_place_name' %>
+
<%= best_in_place @map, :name, :as => :textarea, :activator => "#mapInfoName", :class => 'best_in_place_name' %>
@@ -42,7 +42,7 @@
<% if (authenticated? && @map.authorize_to_edit(user)) || (!authenticated? && @map.desc != "" && @map.desc != nil )%> - <%= best_in_place @map, :desc, :activator => "#mapInfoDesc", :type => :textarea, :nil => "Click to add description...", :classes => 'best_in_place_desc' %> + <%= best_in_place @map, :desc, :activator => "#mapInfoDesc", :as => :textarea, :placeholder => "Click to add description...", :class => 'best_in_place_desc' %> <% end %>
@@ -61,4 +61,4 @@
<% end %> - \ No newline at end of file + From 919fc0a60f1528152b742b8eaa0fe920ea72841a Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 19 Sep 2015 20:01:44 +0800 Subject: [PATCH 015/305] fiddle with topic and mapping controllers so they work again --- app/controllers/main_controller.rb | 8 ++++---- app/controllers/mappings_controller.rb | 4 ++-- app/controllers/topics_controller.rb | 18 +++++++++--------- app/controllers/users_controller.rb | 1 + config/application.rb | 2 ++ 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 46747f71..3d2a341f 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -127,7 +127,7 @@ class MainController < ApplicationController end #read this next line as 'delete a topic if its private and you're either 1. logged out or 2. logged in but not the topic creator - @topics.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } + @topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } render json: autocomplete_array_json(@topics) end @@ -163,7 +163,7 @@ class MainController < ApplicationController end #read this next line as 'delete a map if its private and you're either 1. logged out or 2. logged in but not the map creator - @maps.delete_if {|m| m.permission == "private" && (!authenticated? || (authenticated? && @current.id != m.user_id)) } + @maps.to_a.delete_if {|m| m.permission == "private" && (!authenticated? || (authenticated? && @current.id != m.user_id)) } render json: autocomplete_map_array_json(@maps) end @@ -199,7 +199,7 @@ class MainController < ApplicationController # remove any duplicate synapse types that just differ by # leading or trailing whitespaces collectedDesc = [] - @synapses.delete_if {|s| + @synapses.to_a.delete_if {|s| desc = s.desc == nil || s.desc == "" ? "" : s.desc.strip if collectedDesc.index(desc) == nil collectedDesc.push(desc) @@ -221,7 +221,7 @@ class MainController < ApplicationController @synapses.sort! {|s1,s2| s1.desc <=> s2.desc } #read this next line as 'delete a synapse if its private and you're either 1. logged out or 2. logged in but not the synapse creator - @synapses.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } + @synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } render json: autocomplete_synapse_array_json(@synapses) else diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index 4898ceb4..79d8d80a 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -13,7 +13,7 @@ class MappingsController < ApplicationController # POST /mappings.json def create - @mapping = Mapping.new(params[:mapping]) + @mapping = Mapping.new(mapping_params) @mapping.map.touch(:updated_at) @@ -30,7 +30,7 @@ class MappingsController < ApplicationController @mapping.map.touch(:updated_at) - if @mapping.update_attributes(params[:mapping]) + if @mapping.update_attributes(mapping_params) head :no_content else render json: @mapping.errors, status: :unprocessable_entity diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 7929c5ac..3cc029f9 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -14,7 +14,7 @@ class TopicsController < ApplicationController #read this next line as 'delete a topic if its private and you're either #1. logged out or 2. logged in but not the topic creator - @topics.delete_if {|t| t.permission == "private" && + @topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } else @topics = [] @@ -34,7 +34,7 @@ class TopicsController < ApplicationController respond_to do |format| format.html { @alltopics = ([@topic] + @topic.relatives).delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } # should limit to topics visible to user - @allsynapses = @topic.synapses.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } + @allsynapses = @topic.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } @allcreators = [] @alltopics.each do |t| @@ -63,8 +63,8 @@ class TopicsController < ApplicationController redirect_to root_url, notice: "Access denied. That topic is private." and return end - @alltopics = @topic.relatives.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } - @allsynapses = @topic.synapses.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } + @alltopics = @topic.relatives.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } + @allsynapses = @topic.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } @allcreators = [] @allcreators.push(@topic.user) @alltopics.each do |t| @@ -100,7 +100,7 @@ class TopicsController < ApplicationController @topicsAlreadyHas = params[:network] ? params[:network].split(',') : [] - @alltopics = @topic.relatives.delete_if {|t| + @alltopics = @topic.relatives.to_a.delete_if {|t| @topicsAlreadyHas.index(t.id.to_s) != nil || (t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id))) } @@ -132,7 +132,7 @@ class TopicsController < ApplicationController @topicsAlreadyHas = params[:network] ? params[:network].split(',') : [] - @alltopics = @topic.relatives.delete_if {|t| + @alltopics = @topic.relatives.to_a.delete_if {|t| @topicsAlreadyHas.index(t.id.to_s) != nil || (params[:metacode] && t.metacode_id.to_s != params[:metacode]) || (t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id))) @@ -140,7 +140,7 @@ class TopicsController < ApplicationController @alltopics.uniq! - @allsynapses = @topic.synapses.delete_if {|s| + @allsynapses = @topic.synapses.to_a.delete_if {|s| (s.topic1 == @topic && @alltopics.index(s.topic2) == nil) || (s.topic2 == @topic && @alltopics.index(s.topic1) == nil) } @@ -171,7 +171,7 @@ class TopicsController < ApplicationController # POST /topics # POST /topics.json def create - @topic = Topic.new(params[:topic]) + @topic = Topic.new(topic_params) respond_to do |format| if @topic.save @@ -188,7 +188,7 @@ class TopicsController < ApplicationController @topic = Topic.find(params[:id]) respond_to do |format| - if @topic.update_attributes(params[:topic]) + if @topic.update_attributes(topic_params) format.json { head :no_content } else format.json { render json: @topic.errors, status: :unprocessable_entity } diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 0141e52d..52996f49 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -103,5 +103,6 @@ class UsersController < ApplicationController def user_params params.require(:user).permit(:name, :email, :image, :password, :password_confirmation, :code, :joinedwithcode, :remember_me) + end end diff --git a/config/application.rb b/config/application.rb index 0a5e6b51..f9e0a87d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -44,5 +44,7 @@ module Metamaps # Version of your assets, change this if you want to expire all your assets config.assets.version = '2.0' + + config.active_record.raise_in_transactional_callbacks = true end end From 7c2807097835a50a38034f2581bab695fa6cf7e4 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 19 Sep 2015 20:05:36 +0800 Subject: [PATCH 016/305] fix other controllers needing create/update changed for rails 4 --- app/controllers/maps_controller.rb | 2 +- app/controllers/metacode_sets_controller.rb | 4 ++-- app/controllers/metacodes_controller.rb | 4 ++-- app/controllers/synapses_controller.rb | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index fd2e32b7..c60b8831 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -180,7 +180,7 @@ class MapsController < ApplicationController respond_to do |format| if !@map format.json { render json: "unauthorized" } - elsif @map.update_attributes(params[:map]) + elsif @map.update_attributes(map_params) format.json { head :no_content } else format.json { render json: @map.errors, status: :unprocessable_entity } diff --git a/app/controllers/metacode_sets_controller.rb b/app/controllers/metacode_sets_controller.rb index 376babe5..720076c1 100644 --- a/app/controllers/metacode_sets_controller.rb +++ b/app/controllers/metacode_sets_controller.rb @@ -45,7 +45,7 @@ class MetacodeSetsController < ApplicationController # POST /metacode_sets.json def create @user = current_user - @metacode_set = MetacodeSet.new(params[:metacode_set]) + @metacode_set = MetacodeSet.new(metacode_set_params) @metacode_set.user_id = @user.id respond_to do |format| @@ -70,7 +70,7 @@ class MetacodeSetsController < ApplicationController @metacode_set = MetacodeSet.find(params[:id]) respond_to do |format| - if @metacode_set.update_attributes(params[:metacode_set]) + if @metacode_set.update_attributes(metacode_set_params) # build an array of the IDs of the metacodes currently in the set @currentMetacodes = @metacode_set.metacodes.map{ |m| m.id.to_s } diff --git a/app/controllers/metacodes_controller.rb b/app/controllers/metacodes_controller.rb index 25a7c096..1e5049f1 100644 --- a/app/controllers/metacodes_controller.rb +++ b/app/controllers/metacodes_controller.rb @@ -51,7 +51,7 @@ class MetacodesController < ApplicationController # POST /metacodes # POST /metacodes.json def create - @metacode = Metacode.new(params[:metacode]) + @metacode = Metacode.new(metacode_params) respond_to do |format| if @metacode.save @@ -70,7 +70,7 @@ class MetacodesController < ApplicationController @metacode = Metacode.find(params[:id]) respond_to do |format| - if @metacode.update_attributes(params[:metacode]) + if @metacode.update_attributes(metacode_params) format.html { redirect_to metacodes_url, notice: 'Metacode was successfully updated.' } format.json { head :no_content } else diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index 40941960..3782d336 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -21,7 +21,7 @@ class SynapsesController < ApplicationController # POST /synapses # POST /synapses.json def create - @synapse = Synapse.new(params[:synapse]) + @synapse = Synapse.new(synapse_params) respond_to do |format| if @synapse.save @@ -38,7 +38,7 @@ class SynapsesController < ApplicationController @synapse = Synapse.find(params[:id]) respond_to do |format| - if @synapse.update_attributes(params[:synapse]) + if @synapse.update_attributes(synapse_params) format.json { head :no_content } else format.json { render json: @synapse.errors, status: :unprocessable_entity } From 2369c9ce5efb54378cb6ef9c75ee0e66d3fd00af Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 19 Sep 2015 20:17:56 +0800 Subject: [PATCH 017/305] select metacodes with one query instead of n queries, move logic to application_helper --- app/helpers/application_helper.rb | 16 ++++++++++++++++ app/models/metacode.rb | 3 +-- app/views/maps/_newtopic.html.erb | 13 ++----------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 95f6ba29..fa7876f5 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,2 +1,18 @@ module ApplicationHelper + def get_metacodeset + @m = user.settings.metacodes + set = @m[0].include?("metacodeset") ? MetacodeSet.find(@m[0].sub("metacodeset-","").to_i) : false + return set + end + + def user_metacodes + @m = user.settings.metacodes + set = get_metacodeset + if set + @metacodes = set.metacodes + else + @metacodes = Metacode.where(id: @m).to_a + end + @metacodes.sort! {|m1,m2| m2.name.downcase <=> m1.name.downcase }.rotate!(-1) + end end diff --git a/app/models/metacode.rb b/app/models/metacode.rb index 315f7800..03b0f0c0 100644 --- a/app/models/metacode.rb +++ b/app/models/metacode.rb @@ -13,5 +13,4 @@ class Metacode < ActiveRecord::Base return true if self.metacode_sets.include? metacode_set return false end - -end \ No newline at end of file +end diff --git a/app/views/maps/_newtopic.html.erb b/app/views/maps/_newtopic.html.erb index f22920bb..16b5fecb 100644 --- a/app/views/maps/_newtopic.html.erb +++ b/app/views/maps/_newtopic.html.erb @@ -1,17 +1,8 @@ <%= form_for Topic.new, url: topics_url, remote: true do |form| %>
- <% @m = user.settings.metacodes %> - <% set = @m[0].include?("metacodeset") ? MetacodeSet.find(@m[0].sub("metacodeset-","").to_i) : false %> - <% if set %> - <% @metacodes = set.metacodes %> - <% else %> - <% @metacodes = [] %> - <% @m.each do |m| %> - <% @metacodes.push(Metacode.find(m.to_i)) %> - <% end %> - <% end %> - <% @metacodes.sort! {|m1,m2| m2.name.downcase <=> m1.name.downcase }.rotate!(-1) %> + <% @metacodes = user_metacodes() %> + <% set = get_metacodeset() %> <% @metacodes.each do |metacode| %> <%= metacode.name %> <% end %> From 95e1806500fd84cf6ad745e7775a03ef7f354482 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 19 Sep 2015 20:25:07 +0800 Subject: [PATCH 018/305] cancan => cancancan --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 5eb0d839..7b39be5e 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,7 @@ gem 'rails', '4.2.4' gem 'devise' gem 'redis' gem 'pg' -gem 'cancan' +gem 'cancancan' gem 'formula' gem 'formtastic' gem 'json' diff --git a/Gemfile.lock b/Gemfile.lock index 62c0a085..16ea23f3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -52,7 +52,7 @@ GEM erubis (>= 2.6.6) rack (>= 0.9.0) builder (3.2.2) - cancan (1.6.10) + cancancan (1.12.0) climate_control (0.0.3) activesupport (>= 3.0) cocaine (0.5.7) @@ -196,7 +196,7 @@ DEPENDENCIES aws-sdk best_in_place better_errors - cancan + cancancan coffee-rails devise dotenv From 50d98669397c0a4d60871e5e9391b302c4ca7a0f Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 19 Sep 2015 20:46:10 +0800 Subject: [PATCH 019/305] fix problem with join route --- config/routes.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index 3c79f881..e80b837a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -34,7 +34,7 @@ Metamaps::Application.routes.draw do get 'login' => 'devise/sessions#new', :as => :new_user_session post 'login' => 'devise/sessions#create', :as => :user_session get 'logout' => 'devise/sessions#destroy', :as => :destroy_user_session -# get 'join' => 'devise/registrations#new', :as => :new_user_registration + get 'join' => 'devise/registrations#new', :as => :new_user_registration_path end get 'users/:id/details', to: 'users#details', as: :details From 8b7ec73f48ba976f7e9dafc0bc01ef9998e50e31 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 19 Sep 2015 13:16:07 -0400 Subject: [PATCH 020/305] this enables the vagrant port forwarding --- Gemfile.lock | 3 --- config/boot.rb | 8 ++++++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 16ea23f3..5a181952 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -217,6 +217,3 @@ DEPENDENCIES sass-rails uglifier uservoice-ruby - -BUNDLED WITH - 1.10.6 diff --git a/config/boot.rb b/config/boot.rb index 4489e586..0f0d7c60 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,4 +1,12 @@ require 'rubygems' +require 'rails/commands/server' +module Rails + class Server + def default_options + super.merge(Host: '0.0.0.0', Port: 3000) + end + end +end # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) From 1a01d3b5689915fd2f11e4baea9901104f5881f0 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 19 Sep 2015 13:25:30 -0400 Subject: [PATCH 021/305] configuration for production environments like heroku --- Gemfile | 1 + Gemfile.lock | 6 ++++++ config/environments/development.rb | 1 + config/environments/production.rb | 5 ++++- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 7b39be5e..406c2b84 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,7 @@ end group :production do #this is used on heroku #gem 'rmagick' + gem 'rails_12factor' end group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index 5a181952..8bd00a57 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -151,6 +151,11 @@ GEM loofah (~> 2.0) rails3-jquery-autocomplete (1.0.15) rails (>= 3.2) + rails_12factor (0.0.3) + rails_serve_static_assets + rails_stdout_logging + rails_serve_static_assets (0.0.4) + rails_stdout_logging (0.0.4) railties (4.2.4) actionpack (= 4.2.4) activesupport (= 4.2.4) @@ -213,6 +218,7 @@ DEPENDENCIES quiet_assets rails (= 4.2.4) rails3-jquery-autocomplete + rails_12factor redis sass-rails uglifier diff --git a/config/environments/development.rb b/config/environments/development.rb index 251e12ab..53e8b4fb 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,6 +1,7 @@ Metamaps::Application.configure do # Settings specified here will take precedence over those in config/application.rb + config.log_level = :info config.eager_load = false # In the development environment your application's code is reloaded on diff --git a/config/environments/production.rb b/config/environments/production.rb index 17773071..a515b4ca 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,6 +1,7 @@ Metamaps::Application.configure do # Settings specified here will take precedence over those in config/application.rb + config.log_level = :warn config.eager_load = true config.assets.js_compressor = :uglifier @@ -12,7 +13,9 @@ Metamaps::Application.configure do config.action_controller.perform_caching = true # Disable Rails's static asset server (Apache or nginx will already do this) - config.serve_static_assets = false + config.serve_static_files = true + + config.assets.compile = true # Compress JavaScripts and CSS config.assets.compress = true From b395bd0b5009eb9b894c2c5e344124ac032fea2d Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 22 Sep 2015 22:27:34 +0800 Subject: [PATCH 022/305] replace the obvious spots with asset_path --- app/views/layouts/_templates.html.erb | 6 +++--- app/views/main/home.html.erb | 2 +- app/views/maps/_mapinfobox.html.erb | 2 +- app/views/metacode_sets/_form.html.erb | 2 +- app/views/metacode_sets/index.html.erb | 2 +- app/views/metacodes/index.html.erb | 2 +- app/views/shared/_cheatsheet.html.erb | 2 +- app/views/shared/_filterBox.html.erb | 2 +- app/views/shared/_metacodeoptions.html.erb | 2 +- app/views/shared/_switchmetacodes.html.erb | 2 +- 10 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/views/layouts/_templates.html.erb b/app/views/layouts/_templates.html.erb index afd9de52..4762a271 100644 --- a/app/views/layouts/_templates.html.erb +++ b/app/views/layouts/_templates.html.erb @@ -129,7 +129,7 @@ -
\ No newline at end of file + diff --git a/app/views/main/home.html.erb b/app/views/main/home.html.erb index 2590f2f5..b40fb2a4 100644 --- a/app/views/main/home.html.erb +++ b/app/views/main/home.html.erb @@ -50,4 +50,4 @@ Metamaps.GlobalUI.Search.isOpen = true; Metamaps.GlobalUI.Search.lock(); -<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/maps/_mapinfobox.html.erb b/app/views/maps/_mapinfobox.html.erb index 134cf8ea..43381835 100644 --- a/app/views/maps/_mapinfobox.html.erb +++ b/app/views/maps/_mapinfobox.html.erb @@ -13,7 +13,7 @@
<% if @map.contributors.count == 0 %> - + <% elsif @map.contributors.count == 1 %> <% elsif @map.contributors.count == 2 %> diff --git a/app/views/metacode_sets/_form.html.erb b/app/views/metacode_sets/_form.html.erb index 3a5089b9..e3d1ae40 100644 --- a/app/views/metacode_sets/_form.html.erb +++ b/app/views/metacode_sets/_form.html.erb @@ -86,4 +86,4 @@ { :class => 'button', 'data-bypass' => 'true' } %> <%= f.submit :class => 'add', :onclick => "return Metamaps.Admin.validate();" %>
-<% end %> \ No newline at end of file +<% end %> diff --git a/app/views/metacode_sets/index.html.erb b/app/views/metacode_sets/index.html.erb index 7a93d97f..ba83b0a3 100644 --- a/app/views/metacode_sets/index.html.erb +++ b/app/views/metacode_sets/index.html.erb @@ -34,4 +34,4 @@ <% end %>
- \ No newline at end of file + diff --git a/app/views/metacodes/index.html.erb b/app/views/metacodes/index.html.erb index c99634d4..1ebdb5da 100644 --- a/app/views/metacodes/index.html.erb +++ b/app/views/metacodes/index.html.erb @@ -28,4 +28,4 @@ <% end %> - \ No newline at end of file + diff --git a/app/views/shared/_cheatsheet.html.erb b/app/views/shared/_cheatsheet.html.erb index f0a2ffbc..0af70ec1 100644 --- a/app/views/shared/_cheatsheet.html.erb +++ b/app/views/shared/_cheatsheet.html.erb @@ -61,7 +61,7 @@ Change Topic permission: Click on 'Permission' icon (only for topic creator)
- Open Topic view: Click on icon within topic card bar + Open Topic view: Click on icon within topic card bar
Close 'Topic' card: Click on canvas diff --git a/app/views/shared/_filterBox.html.erb b/app/views/shared/_filterBox.html.erb index b01f6eca..714b1378 100644 --- a/app/views/shared/_filterBox.html.erb +++ b/app/views/shared/_filterBox.html.erb @@ -76,7 +76,7 @@ @synapses.each_with_index do |synapse, index| d = synapse.desc || "" @synapselist += '
  • ' - @synapselist += 'synapse icon

    ' + d + @synapselist += 'synapse icon

    ' + d @synapselist += '

  • ' end @mappers.each_with_index do |mapper, index| diff --git a/app/views/shared/_metacodeoptions.html.erb b/app/views/shared/_metacodeoptions.html.erb index f45a1aa6..25aae79e 100644 --- a/app/views/shared/_metacodeoptions.html.erb +++ b/app/views/shared/_metacodeoptions.html.erb @@ -34,4 +34,4 @@ -
    \ No newline at end of file + diff --git a/app/views/shared/_switchmetacodes.html.erb b/app/views/shared/_switchmetacodes.html.erb index fb3962ef..3d683198 100644 --- a/app/views/shared/_switchmetacodes.html.erb +++ b/app/views/shared/_switchmetacodes.html.erb @@ -73,4 +73,4 @@ \ No newline at end of file + From b7434761157e3e59bd75135b5e93c78f0868d554 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 28 Sep 2015 14:43:09 +0800 Subject: [PATCH 023/305] add qa steps file first draft --- metamaps-qa-steps.txt | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 metamaps-qa-steps.txt diff --git a/metamaps-qa-steps.txt b/metamaps-qa-steps.txt new file mode 100644 index 00000000..36ea193a --- /dev/null +++ b/metamaps-qa-steps.txt @@ -0,0 +1,24 @@ +Metamaps Test Suite + +1) Log in to the interface +2) Create an account using your join code +3) Check your user's "generation" +4) Create three maps: private, public, and another public +5) Change the last map's permissions to commons +6) Change a map's name +7) Create a topic on map #1 +8) Verify (in a private window or another browser) that the second user can't acccess map #1 +9) Create a topic on map #2 +10) Verify that the second user can't edit map #2 +11) Create a topic on map #3 +12) Verify that the second can edit map #3 +13) Pull a topic from map #1 to map #3 +14) Create a private topic on map #1 +15) Verify that the private topic can be pulled from map #1 by the same user +16) Verify that the private topic can't be pulled from map #1 by another user +17) Login as admin. Change metacode sets. +18) Add a number of topics to one of your maps. Reload to see if they are still there. +19) Add a number of synapses to one of your maps. Reload to see if they are still there. +20) Rearrange one of your maps. Reload to see if the layout is preserved. +21) Set the screenshot for one of your maps, and verify the index of maps is updated. +22) Open two browsers on map #3 and verify that realtime editing works. From 0a63b5e79a76938274d50126ead780d80b657cd8 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 1 Oct 2015 11:02:03 +0800 Subject: [PATCH 024/305] fiddle with Gemfile --- Gemfile | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index 406c2b84..0aa716f8 100644 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,10 @@ gem 'dotenv' gem 'paperclip' gem 'aws-sdk' +gem 'jquery-rails' +gem 'jquery-ui-rails' +gem 'jbuilder' + #gem 'therubyracer' #optional #gem 'rb-readline' @@ -44,9 +48,3 @@ group :development, :test do gem 'better_errors' gem 'quiet_assets' end - -gem 'jquery-rails' -gem 'jquery-ui-rails' - -# To use Jbuilder templates for JSON -gem 'jbuilder' From e5c83a2a0cde8152e297b0b441fc71e19a184d0d Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 1 Oct 2015 11:02:39 +0800 Subject: [PATCH 025/305] dependent destroy models for topics/maps/synapses on mappings --- app/controllers/maps_controller.rb | 10 +-------- app/controllers/synapses_controller.rb | 10 +-------- app/controllers/topics_controller.rb | 28 +------------------------- app/models/map.rb | 4 ++-- app/models/synapse.rb | 2 +- app/models/topic.rb | 6 +++--- 6 files changed, 9 insertions(+), 51 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index c60b8831..21946f88 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -218,15 +218,7 @@ class MapsController < ApplicationController @map = Map.find(params[:id]).authorize_to_delete(@current) - if @map - @mappings = @map.mappings - - @mappings.each do |mapping| - mapping.delete - end - - @map.delete - end + @map.delete if @map respond_to do |format| format.json { diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index 3782d336..47d3321e 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -50,15 +50,7 @@ class SynapsesController < ApplicationController def destroy @current = current_user @synapse = Synapse.find(params[:id]).authorize_to_delete(@current) - - if @synapse - @synapse.mappings.each do |m| - m.map.touch(:updated_at) - m.delete - end - - @synapse.delete - end + @synapse.delete if @synapse respond_to do |format| format.json { head :no_content } diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 3cc029f9..d73be190 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -200,33 +200,7 @@ class TopicsController < ApplicationController def destroy @current = current_user @topic = Topic.find(params[:id]).authorize_to_delete(@current) - - if @topic - @synapses = @topic.synapses - @mappings = @topic.mappings - - @synapses.each do |synapse| - synapse.mappings.each do |m| - - @map = m.map - @map.touch(:updated_at) - - m.delete - end - - synapse.delete - end - - @mappings.each do |mapping| - - @map = mapping.map - @map.touch(:updated_at) - - mapping.delete - end - - @topic.delete - end + @topic.delete if @topic respond_to do |format| format.json { head :no_content } diff --git a/app/models/map.rb b/app/models/map.rb index ee949676..6262924e 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -2,8 +2,8 @@ class Map < ActiveRecord::Base belongs_to :user - has_many :topicmappings, -> { Mapping.topicmapping }, class_name: :Mapping - has_many :synapsemappings, -> { Mapping.synapsemapping }, class_name: :Mapping + has_many :topicmappings, -> { Mapping.topicmapping }, class_name: :Mapping, dependent: :destroy + has_many :synapsemappings, -> { Mapping.synapsemapping }, class_name: :Mapping, dependent: :destroy has_many :topics, through: :topicmappings has_many :synapses, through: :synapsemappings diff --git a/app/models/synapse.rb b/app/models/synapse.rb index bf3bdab2..10fba6e9 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -5,7 +5,7 @@ class Synapse < ActiveRecord::Base belongs_to :topic1, :class_name => "Topic", :foreign_key => "node1_id" belongs_to :topic2, :class_name => "Topic", :foreign_key => "node2_id" - has_many :mappings + has_many :mappings, dependent: :destroy has_many :maps, :through => :mappings def user_name diff --git a/app/models/topic.rb b/app/models/topic.rb index 078c633e..d28127fa 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -3,12 +3,12 @@ class Topic < ActiveRecord::Base belongs_to :user - has_many :synapses1, :class_name => 'Synapse', :foreign_key => 'node1_id' - has_many :synapses2, :class_name => 'Synapse', :foreign_key => 'node2_id' + has_many :synapses1, :class_name => 'Synapse', :foreign_key => 'node1_id', dependent: :destroy + has_many :synapses2, :class_name => 'Synapse', :foreign_key => 'node2_id', dependent: :destroy has_many :topics1, :through => :synapses2, :source => :topic1 has_many :topics2, :through => :synapses1, :source => :topic2 - has_many :mappings + has_many :mappings, dependent: :destroy has_many :maps, :through => :mappings # This method associates the attribute ":image" with a file attachment From 8826dfdcf64dd0f0c4460e40655e181f69e6fac1 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 1 Oct 2015 11:14:25 +0800 Subject: [PATCH 026/305] upgrade typeahead to allow new syntax --- app/assets/javascripts/lib/typeahead.js | 2439 +++++++++++++---------- app/assets/javascripts/src/Metamaps.js | 8 +- 2 files changed, 1410 insertions(+), 1037 deletions(-) diff --git a/app/assets/javascripts/lib/typeahead.js b/app/assets/javascripts/lib/typeahead.js index 3a413d68..2b089289 100644 --- a/app/assets/javascripts/lib/typeahead.js +++ b/app/assets/javascripts/lib/typeahead.js @@ -1,659 +1,608 @@ /*! - * typeahead.js 0.9.3 - * https://github.com/twitter/typeahead - * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT + * typeahead.js 0.11.1 + * https://github.com/twitter/typeahead.js + * Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT */ -(function($) { - var VERSION = "0.9.3"; - var utils = { - isMsie: function() { - var match = /(msie) ([\w.]+)/i.exec(navigator.userAgent); - return match ? parseInt(match[2], 10) : false; - }, - isBlankString: function(str) { - return !str || /^\s*$/.test(str); - }, - escapeRegExChars: function(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }, - isString: function(obj) { - return typeof obj === "string"; - }, - isNumber: function(obj) { - return typeof obj === "number"; - }, - isArray: $.isArray, - isFunction: $.isFunction, - isObject: $.isPlainObject, - isUndefined: function(obj) { - return typeof obj === "undefined"; - }, - bind: $.proxy, - bindAll: function(obj) { - var val; - for (var key in obj) { - $.isFunction(val = obj[key]) && (obj[key] = $.proxy(val, obj)); - } - }, - indexOf: function(haystack, needle) { - for (var i = 0; i < haystack.length; i++) { - if (haystack[i] === needle) { - return i; +(function(root, factory) { + if (typeof define === "function" && define.amd) { + define("typeahead.js", [ "jquery" ], function(a0) { + return factory(a0); + }); + } else if (typeof exports === "object") { + module.exports = factory(require("jquery")); + } else { + factory(jQuery); + } +})(this, function($) { + var _ = function() { + "use strict"; + return { + isMsie: function() { + return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; + }, + isBlankString: function(str) { + return !str || /^\s*$/.test(str); + }, + escapeRegExChars: function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }, + isString: function(obj) { + return typeof obj === "string"; + }, + isNumber: function(obj) { + return typeof obj === "number"; + }, + isArray: $.isArray, + isFunction: $.isFunction, + isObject: $.isPlainObject, + isUndefined: function(obj) { + return typeof obj === "undefined"; + }, + isElement: function(obj) { + return !!(obj && obj.nodeType === 1); + }, + isJQuery: function(obj) { + return obj instanceof $; + }, + toStr: function toStr(s) { + return _.isUndefined(s) || s === null ? "" : s + ""; + }, + bind: $.proxy, + each: function(collection, cb) { + $.each(collection, reverseArgs); + function reverseArgs(index, value) { + return cb(value, index); } - } - return -1; - }, - each: $.each, - map: $.map, - filter: $.grep, - every: function(obj, test) { - var result = true; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (!(result = test.call(null, val, key, obj))) { - return false; + }, + map: $.map, + filter: $.grep, + every: function(obj, test) { + var result = true; + if (!obj) { + return result; } - }); - return !!result; - }, - some: function(obj, test) { - var result = false; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (result = test.call(null, val, key, obj)) { - return false; + $.each(obj, function(key, val) { + if (!(result = test.call(null, val, key, obj))) { + return false; + } + }); + return !!result; + }, + some: function(obj, test) { + var result = false; + if (!obj) { + return result; } - }); - return !!result; - }, - mixin: $.extend, - getUniqueId: function() { - var counter = 0; - return function() { - return counter++; - }; - }(), - defer: function(fn) { - setTimeout(fn, 0); - }, - debounce: function(func, wait, immediate) { - var timeout, result; - return function() { - var context = this, args = arguments, later, callNow; - later = function() { - timeout = null; - if (!immediate) { + $.each(obj, function(key, val) { + if (result = test.call(null, val, key, obj)) { + return false; + } + }); + return !!result; + }, + mixin: $.extend, + identity: function(x) { + return x; + }, + clone: function(obj) { + return $.extend(true, {}, obj); + }, + getIdGenerator: function() { + var counter = 0; + return function() { + return counter++; + }; + }, + templatify: function templatify(obj) { + return $.isFunction(obj) ? obj : template; + function template() { + return String(obj); + } + }, + defer: function(fn) { + setTimeout(fn, 0); + }, + debounce: function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments, later, callNow; + later = function() { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + } + }; + callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { result = func.apply(context, args); } + return result; }; - callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - result = func.apply(context, args); - } - return result; - }; - }, - throttle: function(func, wait) { - var context, args, timeout, result, previous, later; - previous = 0; - later = function() { - previous = new Date(); - timeout = null; - result = func.apply(context, args); - }; - return function() { - var now = new Date(), remaining = wait - (now - previous); - context = this; - args = arguments; - if (remaining <= 0) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(context, args); - } else if (!timeout) { - timeout = setTimeout(later, remaining); - } - return result; - }; - }, - tokenizeQuery: function(str) { - return $.trim(str).toLowerCase().split(/[\s]+/); - }, - tokenizeText: function(str) { - return $.trim(str).toLowerCase().split(/[\s\-_]+/); - }, - getProtocol: function() { - return location.protocol; - }, - noop: function() {} - }; - var EventTarget = function() { - var eventSplitter = /\s+/; - return { - on: function(events, callback) { - var event; - if (!callback) { - return this; - } - this._callbacks = this._callbacks || {}; - events = events.split(eventSplitter); - while (event = events.shift()) { - this._callbacks[event] = this._callbacks[event] || []; - this._callbacks[event].push(callback); - } - return this; }, - trigger: function(events, data) { - var event, callbacks; - if (!this._callbacks) { - return this; - } - events = events.split(eventSplitter); - while (event = events.shift()) { - if (callbacks = this._callbacks[event]) { - for (var i = 0; i < callbacks.length; i += 1) { - callbacks[i].call(this, { - type: event, - data: data - }); - } + throttle: function(func, wait) { + var context, args, timeout, result, previous, later; + previous = 0; + later = function() { + previous = new Date(); + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = new Date(), remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); } - } - return this; - } + return result; + }; + }, + stringify: function(val) { + return _.isString(val) ? val : JSON.stringify(val); + }, + noop: function() {} }; }(); + var WWW = function() { + "use strict"; + var defaultClassNames = { + wrapper: "twitter-typeahead", + input: "tt-input", + hint: "tt-hint", + menu: "tt-menu", + dataset: "tt-dataset", + suggestion: "tt-suggestion", + selectable: "tt-selectable", + empty: "tt-empty", + open: "tt-open", + cursor: "tt-cursor", + highlight: "tt-highlight" + }; + return build; + function build(o) { + var www, classes; + classes = _.mixin({}, defaultClassNames, o); + www = { + css: buildCss(), + classes: classes, + html: buildHtml(classes), + selectors: buildSelectors(classes) + }; + return { + css: www.css, + html: www.html, + classes: www.classes, + selectors: www.selectors, + mixin: function(o) { + _.mixin(o, www); + } + }; + } + function buildHtml(c) { + return { + wrapper: '', + menu: '
    ' + }; + } + function buildSelectors(classes) { + var selectors = {}; + _.each(classes, function(v, k) { + selectors[k] = "." + v; + }); + return selectors; + } + function buildCss() { + var css = { + wrapper: { + position: "relative", + display: "inline-block" + }, + hint: { + position: "absolute", + top: "0", + left: "0", + borderColor: "transparent", + boxShadow: "none", + opacity: "1" + }, + input: { + position: "relative", + verticalAlign: "top", + backgroundColor: "transparent" + }, + inputWithNoHint: { + position: "relative", + verticalAlign: "top" + }, + menu: { + position: "absolute", + top: "100%", + left: "0", + zIndex: "100", + display: "none" + }, + ltr: { + left: "0", + right: "auto" + }, + rtl: { + left: "auto", + right: " 0" + } + }; + if (_.isMsie()) { + _.mixin(css.input, { + backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" + }); + } + return css; + } + }(); var EventBus = function() { - var namespace = "typeahead:"; + "use strict"; + var namespace, deprecationMap; + namespace = "typeahead:"; + deprecationMap = { + render: "rendered", + cursorchange: "cursorchanged", + select: "selected", + autocomplete: "autocompleted" + }; function EventBus(o) { if (!o || !o.el) { $.error("EventBus initialized without el"); } this.$el = $(o.el); } - utils.mixin(EventBus.prototype, { + _.mixin(EventBus.prototype, { + _trigger: function(type, args) { + var $e; + $e = $.Event(namespace + type); + (args = args || []).unshift($e); + this.$el.trigger.apply(this.$el, args); + return $e; + }, + before: function(type) { + var args, $e; + args = [].slice.call(arguments, 1); + $e = this._trigger("before" + type, args); + return $e.isDefaultPrevented(); + }, trigger: function(type) { - var args = [].slice.call(arguments, 1); - this.$el.trigger(namespace + type, args); + var deprecatedType; + this._trigger(type, [].slice.call(arguments, 1)); + if (deprecatedType = deprecationMap[type]) { + this._trigger(deprecatedType, [].slice.call(arguments, 1)); + } } }); return EventBus; }(); - var PersistentStorage = function() { - var ls, methods; - try { - ls = window.localStorage; - ls.setItem("~~~", "!"); - ls.removeItem("~~~"); - } catch (err) { - ls = null; - } - function PersistentStorage(namespace) { - this.prefix = [ "__", namespace, "__" ].join(""); - this.ttlKey = "__ttl__"; - this.keyMatcher = new RegExp("^" + this.prefix); - } - if (ls && window.JSON) { - methods = { - _prefix: function(key) { - return this.prefix + key; - }, - _ttlKey: function(key) { - return this._prefix(key) + this.ttlKey; - }, - get: function(key) { - if (this.isExpired(key)) { - this.remove(key); - } - return decode(ls.getItem(this._prefix(key))); - }, - set: function(key, val, ttl) { - if (utils.isNumber(ttl)) { - ls.setItem(this._ttlKey(key), encode(now() + ttl)); - } else { - ls.removeItem(this._ttlKey(key)); - } - return ls.setItem(this._prefix(key), encode(val)); - }, - remove: function(key) { - ls.removeItem(this._ttlKey(key)); - ls.removeItem(this._prefix(key)); - return this; - }, - clear: function() { - var i, key, keys = [], len = ls.length; - for (i = 0; i < len; i++) { - if ((key = ls.key(i)).match(this.keyMatcher)) { - keys.push(key.replace(this.keyMatcher, "")); - } - } - for (i = keys.length; i--; ) { - this.remove(keys[i]); - } - return this; - }, - isExpired: function(key) { - var ttl = decode(ls.getItem(this._ttlKey(key))); - return utils.isNumber(ttl) && now() > ttl ? true : false; - } - }; - } else { - methods = { - get: utils.noop, - set: utils.noop, - remove: utils.noop, - clear: utils.noop, - isExpired: utils.noop - }; - } - utils.mixin(PersistentStorage.prototype, methods); - return PersistentStorage; - function now() { - return new Date().getTime(); - } - function encode(val) { - return JSON.stringify(utils.isUndefined(val) ? null : val); - } - function decode(val) { - return JSON.parse(val); - } - }(); - var RequestCache = function() { - function RequestCache(o) { - utils.bindAll(this); - o = o || {}; - this.sizeLimit = o.sizeLimit || 10; - this.cache = {}; - this.cachedKeysByAge = []; - } - utils.mixin(RequestCache.prototype, { - get: function(url) { - return this.cache[url]; - }, - set: function(url, resp) { - var requestToEvict; - if (this.cachedKeysByAge.length === this.sizeLimit) { - requestToEvict = this.cachedKeysByAge.shift(); - delete this.cache[requestToEvict]; - } - this.cache[url] = resp; - this.cachedKeysByAge.push(url); - } - }); - return RequestCache; - }(); - var Transport = function() { - var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests, requestCache; - function Transport(o) { - utils.bindAll(this); - o = utils.isString(o) ? { - url: o - } : o; - requestCache = requestCache || new RequestCache(); - maxPendingRequests = utils.isNumber(o.maxParallelRequests) ? o.maxParallelRequests : maxPendingRequests || 6; - this.url = o.url; - this.wildcard = o.wildcard || "%QUERY"; - this.filter = o.filter; - this.replace = o.replace; - this.ajaxSettings = { - type: "get", - cache: o.cache, - timeout: o.timeout, - dataType: o.dataType || "json", - beforeSend: o.beforeSend - }; - this._get = (/^throttle$/i.test(o.rateLimitFn) ? utils.throttle : utils.debounce)(this._get, o.rateLimitWait || 300); - } - utils.mixin(Transport.prototype, { - _get: function(url, cb) { - var that = this; - if (belowPendingRequestsThreshold()) { - this._sendRequest(url).done(done); - } else { - this.onDeckRequestArgs = [].slice.call(arguments, 0); - } - function done(resp) { - var data = that.filter ? that.filter(resp) : resp; - cb && cb(data); - requestCache.set(url, resp); - } - }, - _sendRequest: function(url) { - var that = this, jqXhr = pendingRequests[url]; - if (!jqXhr) { - incrementPendingRequests(); - jqXhr = pendingRequests[url] = $.ajax(url, this.ajaxSettings).always(always); - } - return jqXhr; - function always() { - decrementPendingRequests(); - pendingRequests[url] = null; - if (that.onDeckRequestArgs) { - that._get.apply(that, that.onDeckRequestArgs); - that.onDeckRequestArgs = null; - } - } - }, - get: function(query, cb) { - var that = this, encodedQuery = encodeURIComponent(query || ""), url, resp; - cb = cb || utils.noop; - url = this.replace ? this.replace(this.url, encodedQuery) : this.url.replace(this.wildcard, encodedQuery); - if (resp = requestCache.get(url)) { - utils.defer(function() { - cb(that.filter ? that.filter(resp) : resp); - }); - } else { - this._get(url, cb); - } - return !!resp; - } - }); - return Transport; - function incrementPendingRequests() { - pendingRequestsCount++; - } - function decrementPendingRequests() { - pendingRequestsCount--; - } - function belowPendingRequestsThreshold() { - return pendingRequestsCount < maxPendingRequests; - } - }(); - var Dataset = function() { - var keys = { - thumbprint: "thumbprint", - protocol: "protocol", - itemHash: "itemHash", - adjacencyList: "adjacencyList" + var EventEmitter = function() { + "use strict"; + var splitter = /\s+/, nextTick = getNextTick(); + return { + onSync: onSync, + onAsync: onAsync, + off: off, + trigger: trigger }; - function Dataset(o) { - utils.bindAll(this); - if (utils.isString(o.template) && !o.engine) { - $.error("no template engine specified"); + function on(method, types, cb, context) { + var type; + if (!cb) { + return this; } - if (!o.local && !o.prefetch && !o.remote) { - $.error("one of local, prefetch, or remote is required"); + types = types.split(splitter); + cb = context ? bindContext(cb, context) : cb; + this._callbacks = this._callbacks || {}; + while (type = types.shift()) { + this._callbacks[type] = this._callbacks[type] || { + sync: [], + async: [] + }; + this._callbacks[type][method].push(cb); } - this.name = o.name || utils.getUniqueId(); - this.limit = o.limit || 5; - this.minLength = o.minLength || 1; - this.header = o.header; - this.footer = o.footer; - this.valueKey = o.valueKey || "value"; - this.template = compileTemplate(o.template, o.engine, this.valueKey); - this.local = o.local; - this.prefetch = o.prefetch; - this.remote = o.remote; - this.itemHash = {}; - this.adjacencyList = {}; - this.storage = o.name ? new PersistentStorage(o.name) : null; + return this; } - utils.mixin(Dataset.prototype, { - _processLocalData: function(data) { - this._mergeProcessedData(this._processData(data)); - }, - _loadPrefetchData: function(o) { - var that = this, thumbprint = VERSION + (o.thumbprint || ""), storedThumbprint, storedProtocol, storedItemHash, storedAdjacencyList, isExpired, deferred; - if (this.storage) { - storedThumbprint = this.storage.get(keys.thumbprint); - storedProtocol = this.storage.get(keys.protocol); - storedItemHash = this.storage.get(keys.itemHash); - storedAdjacencyList = this.storage.get(keys.adjacencyList); - } - isExpired = storedThumbprint !== thumbprint || storedProtocol !== utils.getProtocol(); - o = utils.isString(o) ? { - url: o - } : o; - o.ttl = utils.isNumber(o.ttl) ? o.ttl : 24 * 60 * 60 * 1e3; - if (storedItemHash && storedAdjacencyList && !isExpired) { - this._mergeProcessedData({ - itemHash: storedItemHash, - adjacencyList: storedAdjacencyList - }); - deferred = $.Deferred().resolve(); - } else { - deferred = $.getJSON(o.url).done(processPrefetchData); - } - return deferred; - function processPrefetchData(data) { - var filteredData = o.filter ? o.filter(data) : data, processedData = that._processData(filteredData), itemHash = processedData.itemHash, adjacencyList = processedData.adjacencyList; - if (that.storage) { - that.storage.set(keys.itemHash, itemHash, o.ttl); - that.storage.set(keys.adjacencyList, adjacencyList, o.ttl); - that.storage.set(keys.thumbprint, thumbprint, o.ttl); - that.storage.set(keys.protocol, utils.getProtocol(), o.ttl); - } - that._mergeProcessedData(processedData); - } - }, - _transformDatum: function(datum) { - var value = utils.isString(datum) ? datum : datum[this.valueKey], tokens = datum.tokens || utils.tokenizeText(value), item = { - value: value, - tokens: tokens - }; - if (utils.isString(datum)) { - item.datum = {}; - item.datum[this.valueKey] = datum; - } else { - item.datum = datum; - } - item.tokens = utils.filter(item.tokens, function(token) { - return !utils.isBlankString(token); - }); - item.tokens = utils.map(item.tokens, function(token) { - return token.toLowerCase(); - }); - return item; - }, - _processData: function(data) { - var that = this, itemHash = {}, adjacencyList = {}; - utils.each(data, function(i, datum) { - var item = that._transformDatum(datum), id = utils.getUniqueId(item.value); - itemHash[id] = item; - utils.each(item.tokens, function(i, token) { - var character = token.charAt(0), adjacency = adjacencyList[character] || (adjacencyList[character] = [ id ]); - !~utils.indexOf(adjacency, id) && adjacency.push(id); - }); - }); - return { - itemHash: itemHash, - adjacencyList: adjacencyList - }; - }, - _mergeProcessedData: function(processedData) { - var that = this; - utils.mixin(this.itemHash, processedData.itemHash); - utils.each(processedData.adjacencyList, function(character, adjacency) { - var masterAdjacency = that.adjacencyList[character]; - that.adjacencyList[character] = masterAdjacency ? masterAdjacency.concat(adjacency) : adjacency; - }); - }, - _getLocalSuggestions: function(terms) { - var that = this, firstChars = [], lists = [], shortestList, suggestions = []; - utils.each(terms, function(i, term) { - var firstChar = term.charAt(0); - !~utils.indexOf(firstChars, firstChar) && firstChars.push(firstChar); - }); - utils.each(firstChars, function(i, firstChar) { - var list = that.adjacencyList[firstChar]; - if (!list) { - return false; - } - lists.push(list); - if (!shortestList || list.length < shortestList.length) { - shortestList = list; - } - }); - if (lists.length < firstChars.length) { - return []; - } - utils.each(shortestList, function(i, id) { - var item = that.itemHash[id], isCandidate, isMatch; - isCandidate = utils.every(lists, function(list) { - return ~utils.indexOf(list, id); - }); - isMatch = isCandidate && utils.every(terms, function(term) { - return utils.some(item.tokens, function(token) { - return token.indexOf(term) === 0; - }); - }); - isMatch && suggestions.push(item); - }); - return suggestions; - }, - initialize: function() { - var deferred; - this.local && this._processLocalData(this.local); - this.transport = this.remote ? new Transport(this.remote) : null; - deferred = this.prefetch ? this._loadPrefetchData(this.prefetch) : $.Deferred().resolve(); - this.local = this.prefetch = this.remote = null; - this.initialize = function() { - return deferred; - }; - return deferred; - }, - getSuggestions: function(query, cb) { - var that = this, terms, suggestions, cacheHit = false; - if (query.length < this.minLength) { - return; - } - terms = utils.tokenizeQuery(query); - suggestions = this._getLocalSuggestions(terms).slice(0, this.limit); - if (suggestions.length < this.limit && this.transport) { - cacheHit = this.transport.get(query, processRemoteData); - } - !cacheHit && cb && cb(suggestions); - function processRemoteData(data) { - suggestions = suggestions.slice(0); - utils.each(data, function(i, datum) { - var item = that._transformDatum(datum), isDuplicate; - isDuplicate = utils.some(suggestions, function(suggestion) { - //return item.value === suggestion.value; - return false; - }); - !isDuplicate && suggestions.push(item); - return suggestions.length < that.limit; - }); - cb && cb(suggestions); - } + function onAsync(types, cb, context) { + return on.call(this, "async", types, cb, context); + } + function onSync(types, cb, context) { + return on.call(this, "sync", types, cb, context); + } + function off(types) { + var type; + if (!this._callbacks) { + return this; } - }); - return Dataset; - function compileTemplate(template, engine, valueKey) { - var renderFn, compiledTemplate; - if (utils.isFunction(template)) { - renderFn = template; - } else if (utils.isString(template)) { - compiledTemplate = engine.compile(template); - renderFn = utils.bind(compiledTemplate.render, compiledTemplate); + types = types.split(splitter); + while (type = types.shift()) { + delete this._callbacks[type]; + } + return this; + } + function trigger(types) { + var type, callbacks, args, syncFlush, asyncFlush; + if (!this._callbacks) { + return this; + } + types = types.split(splitter); + args = [].slice.call(arguments, 1); + while ((type = types.shift()) && (callbacks = this._callbacks[type])) { + syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args)); + asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args)); + syncFlush() && nextTick(asyncFlush); + } + return this; + } + function getFlush(callbacks, context, args) { + return flush; + function flush() { + var cancelled; + for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) { + cancelled = callbacks[i].apply(context, args) === false; + } + return !cancelled; + } + } + function getNextTick() { + var nextTickFn; + if (window.setImmediate) { + nextTickFn = function nextTickSetImmediate(fn) { + setImmediate(function() { + fn(); + }); + }; } else { - renderFn = function(context) { - return "

    " + context[valueKey] + "

    "; + nextTickFn = function nextTickSetTimeout(fn) { + setTimeout(function() { + fn(); + }, 0); }; } - return renderFn; + return nextTickFn; + } + function bindContext(fn, context) { + return fn.bind ? fn.bind(context) : function() { + fn.apply(context, [].slice.call(arguments, 0)); + }; } }(); - var InputView = function() { - function InputView(o) { - var that = this; - utils.bindAll(this); - this.specialKeyCodeMap = { - // START METAMAPS CODE - //9: "tab", - // END METAMAPS CODE - 27: "esc", - 37: "left", - 39: "right", - 13: "enter", - 38: "up", - 40: "down" - }; - this.$hint = $(o.hint); - this.$input = $(o.input).on("blur.tt", this._handleBlur).on("focus.tt", this._handleFocus).on("keydown.tt", this._handleSpecialKeyEvent); - if (!utils.isMsie()) { - this.$input.on("input.tt", this._compareQueryToInputValue); - } else { - this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { - if (that.specialKeyCodeMap[$e.which || $e.keyCode]) { - return; - } - utils.defer(that._compareQueryToInputValue); - }); + var highlight = function(doc) { + "use strict"; + var defaults = { + node: null, + pattern: null, + tagName: "strong", + className: null, + wordsOnly: false, + caseSensitive: false + }; + return function hightlight(o) { + var regex; + o = _.mixin({}, defaults, o); + if (!o.node || !o.pattern) { + return; } - this.query = this.$input.val(); - this.$overflowHelper = buildOverflowHelper(this.$input); + o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; + regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); + traverse(o.node, hightlightTextNode); + function hightlightTextNode(textNode) { + var match, patternNode, wrapperNode; + if (match = regex.exec(textNode.data)) { + wrapperNode = doc.createElement(o.tagName); + o.className && (wrapperNode.className = o.className); + patternNode = textNode.splitText(match.index); + patternNode.splitText(match[0].length); + wrapperNode.appendChild(patternNode.cloneNode(true)); + textNode.parentNode.replaceChild(wrapperNode, patternNode); + } + return !!match; + } + function traverse(el, hightlightTextNode) { + var childNode, TEXT_NODE_TYPE = 3; + for (var i = 0; i < el.childNodes.length; i++) { + childNode = el.childNodes[i]; + if (childNode.nodeType === TEXT_NODE_TYPE) { + i += hightlightTextNode(childNode) ? 1 : 0; + } else { + traverse(childNode, hightlightTextNode); + } + } + } + }; + function getRegex(patterns, caseSensitive, wordsOnly) { + var escapedPatterns = [], regexStr; + for (var i = 0, len = patterns.length; i < len; i++) { + escapedPatterns.push(_.escapeRegExChars(patterns[i])); + } + regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; + return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); } - utils.mixin(InputView.prototype, EventTarget, { - _handleFocus: function() { + }(window.document); + var Input = function() { + "use strict"; + var specialKeyCodeMap; + specialKeyCodeMap = { + 9: "tab", + 27: "esc", + 37: "left", + 39: "right", + 13: "enter", + 38: "up", + 40: "down" + }; + function Input(o, www) { + o = o || {}; + if (!o.input) { + $.error("input is missing"); + } + www.mixin(this); + this.$hint = $(o.hint); + this.$input = $(o.input); + this.query = this.$input.val(); + this.queryWhenFocused = this.hasFocus() ? this.query : null; + this.$overflowHelper = buildOverflowHelper(this.$input); + this._checkLanguageDirection(); + if (this.$hint.length === 0) { + this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; + } + } + Input.normalizeQuery = function(str) { + return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " "); + }; + _.mixin(Input.prototype, EventEmitter, { + _onBlur: function onBlur() { + this.resetInputValue(); + this.trigger("blurred"); + }, + _onFocus: function onFocus() { + this.queryWhenFocused = this.query; this.trigger("focused"); }, - _handleBlur: function() { - this.trigger("blured"); - }, - _handleSpecialKeyEvent: function($e) { - var keyName = this.specialKeyCodeMap[$e.which || $e.keyCode]; - keyName && this.trigger(keyName + "Keyed", $e); - }, - _compareQueryToInputValue: function() { - var inputValue = this.getInputValue(), isSameQuery = compareQueries(this.query, inputValue), isSameQueryExceptWhitespace = isSameQuery ? this.query.length !== inputValue.length : false; - if (isSameQueryExceptWhitespace) { - this.trigger("whitespaceChanged", { - value: this.query - }); - } else if (!isSameQuery) { - this.trigger("queryChanged", { - value: this.query = inputValue - }); + _onKeydown: function onKeydown($e) { + var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; + this._managePreventDefault(keyName, $e); + if (keyName && this._shouldTrigger(keyName, $e)) { + this.trigger(keyName + "Keyed", $e); } }, - destroy: function() { - this.$hint.off(".tt"); - this.$input.off(".tt"); - this.$hint = this.$input = this.$overflowHelper = null; + _onInput: function onInput() { + this._setQuery(this.getInputValue()); + this.clearHintIfInvalid(); + this._checkLanguageDirection(); }, - focus: function() { + _managePreventDefault: function managePreventDefault(keyName, $e) { + var preventDefault; + switch (keyName) { + case "up": + case "down": + preventDefault = !withModifier($e); + break; + + default: + preventDefault = false; + } + preventDefault && $e.preventDefault(); + }, + _shouldTrigger: function shouldTrigger(keyName, $e) { + var trigger; + switch (keyName) { + case "tab": + trigger = !withModifier($e); + break; + + default: + trigger = true; + } + return trigger; + }, + _checkLanguageDirection: function checkLanguageDirection() { + var dir = (this.$input.css("direction") || "ltr").toLowerCase(); + if (this.dir !== dir) { + this.dir = dir; + this.$hint.attr("dir", dir); + this.trigger("langDirChanged", dir); + } + }, + _setQuery: function setQuery(val, silent) { + var areEquivalent, hasDifferentWhitespace; + areEquivalent = areQueriesEquivalent(val, this.query); + hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false; + this.query = val; + if (!silent && !areEquivalent) { + this.trigger("queryChanged", this.query); + } else if (!silent && hasDifferentWhitespace) { + this.trigger("whitespaceChanged", this.query); + } + }, + bind: function() { + var that = this, onBlur, onFocus, onKeydown, onInput; + onBlur = _.bind(this._onBlur, this); + onFocus = _.bind(this._onFocus, this); + onKeydown = _.bind(this._onKeydown, this); + onInput = _.bind(this._onInput, this); + this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); + if (!_.isMsie() || _.isMsie() > 9) { + this.$input.on("input.tt", onInput); + } else { + this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { + if (specialKeyCodeMap[$e.which || $e.keyCode]) { + return; + } + _.defer(_.bind(that._onInput, that, $e)); + }); + } + return this; + }, + focus: function focus() { this.$input.focus(); }, - blur: function() { + blur: function blur() { this.$input.blur(); }, - getQuery: function() { - return this.query; + getLangDir: function getLangDir() { + return this.dir; }, - setQuery: function(query) { - this.query = query; + getQuery: function getQuery() { + return this.query || ""; }, - getInputValue: function() { + setQuery: function setQuery(val, silent) { + this.setInputValue(val); + this._setQuery(val, silent); + }, + hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() { + return this.query !== this.queryWhenFocused; + }, + getInputValue: function getInputValue() { return this.$input.val(); }, - setInputValue: function(value, silent) { + setInputValue: function setInputValue(value) { this.$input.val(value); - !silent && this._compareQueryToInputValue(); + this.clearHintIfInvalid(); + this._checkLanguageDirection(); }, - getHintValue: function() { + resetInputValue: function resetInputValue() { + this.setInputValue(this.query); + }, + getHint: function getHint() { return this.$hint.val(); }, - setHintValue: function(value) { + setHint: function setHint(value) { this.$hint.val(value); }, - getLanguageDirection: function() { - return (this.$input.css("direction") || "ltr").toLowerCase(); + clearHint: function clearHint() { + this.setHint(""); }, - isOverflow: function() { + clearHintIfInvalid: function clearHintIfInvalid() { + var val, hint, valIsPrefixOfHint, isValid; + val = this.getInputValue(); + hint = this.getHint(); + valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; + isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow(); + !isValid && this.clearHint(); + }, + hasFocus: function hasFocus() { + return this.$input.is(":focus"); + }, + hasOverflow: function hasOverflow() { + var constraint = this.$input.width() - 2; this.$overflowHelper.text(this.getInputValue()); - return this.$overflowHelper.width() > this.$input.width(); + return this.$overflowHelper.width() >= constraint; }, isCursorAtEnd: function() { - var valueLength = this.$input.val().length, selectionStart = this.$input[0].selectionStart, range; - if (utils.isNumber(selectionStart)) { + var valueLength, selectionStart, range; + valueLength = this.$input.val().length; + selectionStart = this.$input[0].selectionStart; + if (_.isNumber(selectionStart)) { return selectionStart === valueLength; } else if (document.selection) { range = document.selection.createRange(); @@ -661,15 +610,20 @@ return valueLength === range.text.length; } return true; + }, + destroy: function destroy() { + this.$hint.off(".tt"); + this.$input.off(".tt"); + this.$overflowHelper.remove(); + this.$hint = this.$input = this.$overflowHelper = $("
    "); } }); - return InputView; + return Input; function buildOverflowHelper($input) { - return $("").css({ + return $('').css({ position: "absolute", - left: "-9999px", visibility: "hidden", - whiteSpace: "nowrap", + whiteSpace: "pre", fontFamily: $input.css("font-family"), fontSize: $input.css("font-size"), fontStyle: $input.css("font-style"), @@ -682,484 +636,903 @@ textTransform: $input.css("text-transform") }).insertAfter($input); } - function compareQueries(a, b) { - a = (a || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); - b = (b || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); - return a === b; + function areQueriesEquivalent(a, b) { + return Input.normalizeQuery(a) === Input.normalizeQuery(b); + } + function withModifier($e) { + return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; } }(); - var DropdownView = function() { - var html = { - suggestionsList: '' - }, css = { - suggestionsList: { - display: "block" - }, - suggestion: { - whiteSpace: "nowrap", - cursor: "pointer" - }, - suggestionChild: { - whiteSpace: "normal" - } + var Dataset = function() { + "use strict"; + var keys, nameGenerator; + keys = { + val: "tt-selectable-display", + obj: "tt-selectable-object" }; - function DropdownView(o) { - utils.bindAll(this); - this.isOpen = false; - this.isEmpty = true; - this.isMouseOverDropdown = false; - this.$menu = $(o.menu).on("mouseenter.tt", this._handleMouseenter).on("mouseleave.tt", this._handleMouseleave).on("click.tt", ".tt-suggestion", this._handleSelection).on("mouseover.tt", ".tt-suggestion", this._handleMouseover); + nameGenerator = _.getIdGenerator(); + function Dataset(o, www) { + o = o || {}; + o.templates = o.templates || {}; + o.templates.notFound = o.templates.notFound || o.templates.empty; + if (!o.source) { + $.error("missing source"); + } + if (!o.node) { + $.error("missing node"); + } + if (o.name && !isValidName(o.name)) { + $.error("invalid dataset name: " + o.name); + } + www.mixin(this); + this.highlight = !!o.highlight; + this.name = o.name || nameGenerator(); + this.limit = o.limit || 5; + this.displayFn = getDisplayFn(o.display || o.displayKey); + this.templates = getTemplates(o.templates, this.displayFn); + this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source; + this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async; + this._resetLastSuggestion(); + this.$el = $(o.node).addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name); } - utils.mixin(DropdownView.prototype, EventTarget, { - _handleMouseenter: function() { - this.isMouseOverDropdown = true; - }, - _handleMouseleave: function() { - this.isMouseOverDropdown = false; - - // START METAMAPS CODE - this._getSuggestions().removeClass("tt-is-under-cursor"); - this._getSuggestions().removeClass("tt-is-under-mouse-cursor"); - // END METAMAPS CODE - }, - _handleMouseover: function($e) { - var $suggestion = $($e.currentTarget); - this._getSuggestions().removeClass("tt-is-under-cursor"); - // START METAMAPS CODE - this._getSuggestions().removeClass("tt-is-under-mouse-cursor"); - $suggestion.addClass("tt-is-under-mouse-cursor"); - // ORIGINAL CODE $suggestion.addClass("tt-is-under-cursor"); - }, - _handleSelection: function($e) { - var $suggestion = $($e.currentTarget); - this.trigger("suggestionSelected", extractSuggestion($suggestion)); - }, - _show: function() { - this.$menu.css("display", "block"); - }, - _hide: function() { - this.$menu.hide(); - }, - _moveCursor: function(increment) { - var $suggestions, $cur, nextIndex, $underCursor; - if (!this.isVisible()) { - return; - } - $suggestions = this._getSuggestions(); - $cur = $suggestions.filter(".tt-is-under-cursor"); - $cur.removeClass("tt-is-under-cursor"); - nextIndex = $suggestions.index($cur) + increment; - nextIndex = (nextIndex + 1) % ($suggestions.length + 1) - 1; - if (nextIndex === -1) { - this.trigger("cursorRemoved"); - return; - } else if (nextIndex < -1) { - nextIndex = $suggestions.length - 1; - } - $underCursor = $suggestions.eq(nextIndex).addClass("tt-is-under-cursor"); - this._ensureVisibility($underCursor); - this.trigger("cursorMoved", extractSuggestion($underCursor)); - }, - _getSuggestions: function() { - return this.$menu.find(".tt-suggestions > .tt-suggestion"); - }, - _ensureVisibility: function($el) { - var menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10), menuScrollTop = this.$menu.scrollTop(), elTop = $el.position().top, elBottom = elTop + $el.outerHeight(true); - if (elTop < 0) { - this.$menu.scrollTop(menuScrollTop + elTop); - } else if (menuHeight < elBottom) { - this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight)); - } - }, - destroy: function() { - this.$menu.off(".tt"); - this.$menu = null; - }, - isVisible: function() { - return this.isOpen && !this.isEmpty; - }, - closeUnlessMouseIsOverDropdown: function() { - if (!this.isMouseOverDropdown) { - this.close(); - } - }, - close: function() { - if (this.isOpen) { - this.isOpen = false; - this.isMouseOverDropdown = false; - this._hide(); - this.$menu.find(".tt-suggestions > .tt-suggestion").removeClass("tt-is-under-cursor"); - this.trigger("closed"); - } - }, - open: function() { - if (!this.isOpen) { - this.isOpen = true; - !this.isEmpty && this._show(); - this.trigger("opened"); - } - }, - setLanguageDirection: function(dir) { - var ltrCss = { - left: "0", - right: "auto" - }, rtlCss = { - left: "auto", - right: " 0" + Dataset.extractData = function extractData(el) { + var $el = $(el); + if ($el.data(keys.obj)) { + return { + val: $el.data(keys.val) || "", + obj: $el.data(keys.obj) || null }; - dir === "ltr" ? this.$menu.css(ltrCss) : this.$menu.css(rtlCss); - }, - moveCursorUp: function() { - this._moveCursor(-1); - }, - moveCursorDown: function() { - this._moveCursor(+1); - }, - getSuggestionUnderCursor: function() { - var $suggestion = this._getSuggestions().filter(".tt-is-under-cursor").first(); - return $suggestion.length > 0 ? extractSuggestion($suggestion) : null; - }, - getFirstSuggestion: function() { - var $suggestion = this._getSuggestions().first(); - return $suggestion.length > 0 ? extractSuggestion($suggestion) : null; - }, - renderSuggestions: function(dataset, suggestions) { - var datasetClassName = "tt-dataset-" + dataset.name, wrapper = '
    %body
    ', compiledHtml, $suggestionsList, $dataset = this.$menu.find("." + datasetClassName), elBuilder, fragment, $el; - if ($dataset.length === 0) { - $suggestionsList = $(html.suggestionsList).css(css.suggestionsList); - $dataset = $("
    ").addClass(datasetClassName).append(dataset.header).append($suggestionsList).append(dataset.footer).appendTo(this.$menu); - } - if (suggestions.length > 0) { - this.isEmpty = false; - this.isOpen && this._show(); - elBuilder = document.createElement("div"); - fragment = document.createDocumentFragment(); - utils.each(suggestions, function(i, suggestion) { - suggestion.dataset = dataset.name; - compiledHtml = dataset.template(suggestion.datum); - elBuilder.innerHTML = wrapper.replace("%body", compiledHtml); - $el = $(elBuilder.firstChild).css(css.suggestion).data("suggestion", suggestion); - $el.children().each(function() { - $(this).css(css.suggestionChild); - }); - fragment.appendChild($el[0]); - }); - $dataset.show().find(".tt-suggestions").html(fragment); - } else { - this.clearSuggestions(dataset.name); - } - this.trigger("suggestionsRendered"); - }, - clearSuggestions: function(datasetName) { - var $datasets = datasetName ? this.$menu.find(".tt-dataset-" + datasetName) : this.$menu.find('[class^="tt-dataset-"]'), $suggestions = $datasets.find(".tt-suggestions"); - $datasets.hide(); - $suggestions.empty(); - if (this._getSuggestions().length === 0) { - this.isEmpty = true; - this._hide(); - } - } - }); - return DropdownView; - function extractSuggestion($el) { - return $el.data("suggestion"); - } - }(); - var TypeaheadView = function() { - var html = { - wrapper: '', - hint: '', - dropdown: '' - }, css = { - wrapper: { - position: "relative", - display: "inline-block" - }, - hint: { - position: "absolute", - top: "0", - left: "0", - borderColor: "transparent", - boxShadow: "none" - }, - query: { - position: "relative", - verticalAlign: "top", - backgroundColor: "transparent" - }, - dropdown: { - position: "absolute", - top: "100%", - left: "0", - zIndex: "100", - display: "none" } + return null; }; - if (utils.isMsie()) { - utils.mixin(css.query, { - backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" - }); - } - if (utils.isMsie() && utils.isMsie() <= 7) { - utils.mixin(css.wrapper, { - display: "inline", - zoom: "1" - }); - utils.mixin(css.query, { - marginTop: "-1px" - }); - } - function TypeaheadView(o) { - var $menu, $input, $hint; - utils.bindAll(this); - this.$node = buildDomStructure(o.input); - this.datasets = o.datasets; - this.dir = null; - this.eventBus = o.eventBus; - $menu = this.$node.find(".tt-dropdown-menu"); - $input = this.$node.find(".tt-query"); - $hint = this.$node.find(".tt-hint"); - this.dropdownView = new DropdownView({ - menu: $menu - }).on("suggestionSelected", this._handleSelection).on("cursorMoved", this._clearHint).on("cursorMoved", this._setInputValueToSuggestionUnderCursor).on("cursorRemoved", this._setInputValueToQuery).on("cursorRemoved", this._updateHint).on("suggestionsRendered", this._updateHint).on("opened", this._updateHint).on("closed", this._clearHint).on("opened closed", this._propagateEvent); - // START METAMAPS CODE - this.dropdownView.on('suggestionsRendered', this._suggestionsRendered); - // END METAMAPS CODE - - this.inputView = new InputView({ - input: $input, - hint: $hint - }).on("focused", this._openDropdown).on("blured", this._closeDropdown).on("blured", this._setInputValueToQuery).on("enterKeyed tabKeyed", this._handleSelection).on("queryChanged", this._clearHint).on("queryChanged", this._clearSuggestions).on("queryChanged", this._getSuggestions).on("whitespaceChanged", this._updateHint).on("queryChanged whitespaceChanged", this._openDropdown).on("queryChanged whitespaceChanged", this._setLanguageDirection).on("escKeyed", this._closeDropdown).on("escKeyed", this._setInputValueToQuery).on("tabKeyed upKeyed downKeyed", this._managePreventDefault).on("upKeyed downKeyed", this._moveDropdownCursor).on("upKeyed downKeyed", this._openDropdown).on("tabKeyed leftKeyed rightKeyed", this._autocomplete); - // START METAMAPS CODE - this.inputView.on('queryChanged', this._queryChanged); - // END METAMAPS CODE - } - utils.mixin(TypeaheadView.prototype, EventTarget, { - _managePreventDefault: function(e) { - var $e = e.data, hint, inputValue, preventDefault = false; - switch (e.type) { - case "tabKeyed": - hint = this.inputView.getHintValue(); - inputValue = this.inputView.getInputValue(); - preventDefault = hint && hint !== inputValue; - break; - - case "upKeyed": - case "downKeyed": - preventDefault = !$e.shiftKey && !$e.ctrlKey && !$e.metaKey; - break; + _.mixin(Dataset.prototype, EventEmitter, { + _overwrite: function overwrite(query, suggestions) { + suggestions = suggestions || []; + if (suggestions.length) { + this._renderSuggestions(query, suggestions); + } else if (this.async && this.templates.pending) { + this._renderPending(query); + } else if (!this.async && this.templates.notFound) { + this._renderNotFound(query); + } else { + this._empty(); } - preventDefault && $e.preventDefault(); + this.trigger("rendered", this.name, suggestions, false); }, - _setLanguageDirection: function() { - var dir = this.inputView.getLanguageDirection(); - if (dir !== this.dir) { - this.dir = dir; - this.$node.css("direction", dir); - this.dropdownView.setLanguageDirection(dir); + _append: function append(query, suggestions) { + suggestions = suggestions || []; + if (suggestions.length && this.$lastSuggestion.length) { + this._appendSuggestions(query, suggestions); + } else if (suggestions.length) { + this._renderSuggestions(query, suggestions); + } else if (!this.$lastSuggestion.length && this.templates.notFound) { + this._renderNotFound(query); } + this.trigger("rendered", this.name, suggestions, true); }, - // START METAMAPS CODE - _suggestionsRendered: function() { - this.eventBus.trigger('suggestionsRendered'); + _renderSuggestions: function renderSuggestions(query, suggestions) { + var $fragment; + $fragment = this._getSuggestionsFragment(query, suggestions); + this.$lastSuggestion = $fragment.children().last(); + this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions)); }, - _queryChanged: function() { - this.eventBus.trigger('queryChanged'); + _appendSuggestions: function appendSuggestions(query, suggestions) { + var $fragment, $lastSuggestion; + $fragment = this._getSuggestionsFragment(query, suggestions); + $lastSuggestion = $fragment.children().last(); + this.$lastSuggestion.after($fragment); + this.$lastSuggestion = $lastSuggestion; }, - // END METAMAPS CODE - _updateHint: function() { - var suggestion = this.dropdownView.getFirstSuggestion(), hint = suggestion ? suggestion.value : null, dropdownIsVisible = this.dropdownView.isVisible(), inputHasOverflow = this.inputView.isOverflow(), inputValue, query, escapedQuery, beginsWithQuery, match; - if (hint && dropdownIsVisible && !inputHasOverflow) { - inputValue = this.inputView.getInputValue(); - query = inputValue.replace(/\s{2,}/g, " ").replace(/^\s+/g, ""); - escapedQuery = utils.escapeRegExChars(query); - beginsWithQuery = new RegExp("^(?:" + escapedQuery + ")(.*$)", "i"); - match = beginsWithQuery.exec(hint); - this.inputView.setHintValue(inputValue + (match ? match[1] : "")); - } + _renderPending: function renderPending(query) { + var template = this.templates.pending; + this._resetLastSuggestion(); + template && this.$el.html(template({ + query: query, + dataset: this.name + })); }, - _clearHint: function() { - this.inputView.setHintValue(""); + _renderNotFound: function renderNotFound(query) { + var template = this.templates.notFound; + this._resetLastSuggestion(); + template && this.$el.html(template({ + query: query, + dataset: this.name + })); }, - _clearSuggestions: function() { - this.dropdownView.clearSuggestions(); + _empty: function empty() { + this.$el.empty(); + this._resetLastSuggestion(); }, - _setInputValueToQuery: function() { - this.inputView.setInputValue(this.inputView.getQuery()); - }, - _setInputValueToSuggestionUnderCursor: function(e) { - var suggestion = e.data; - this.inputView.setInputValue(suggestion.value, true); - }, - _openDropdown: function() { - this.dropdownView.open(); - }, - _closeDropdown: function(e) { - this.dropdownView[e.type === "blured" ? "closeUnlessMouseIsOverDropdown" : "close"](); - }, - _moveDropdownCursor: function(e) { - var $e = e.data; - if (!$e.shiftKey && !$e.ctrlKey && !$e.metaKey) { - this.dropdownView[e.type === "upKeyed" ? "moveCursorUp" : "moveCursorDown"](); - } - }, - _handleSelection: function(e) { - var byClick = e.type === "suggestionSelected", suggestion = byClick ? e.data : this.dropdownView.getSuggestionUnderCursor(); - if (suggestion) { - this.inputView.setInputValue(suggestion.value); - byClick ? this.inputView.focus() : e.data.preventDefault(); - byClick && utils.isMsie() ? utils.defer(this.dropdownView.close) : this.dropdownView.close(); - this.eventBus.trigger("selected", suggestion.datum, suggestion.dataset); - } - }, - _getSuggestions: function() { - var that = this, query = this.inputView.getQuery(); - if (utils.isBlankString(query)) { - return; - } - utils.each(this.datasets, function(i, dataset) { - dataset.getSuggestions(query, function(suggestions) { - if (query === that.inputView.getQuery()) { - that.dropdownView.renderSuggestions(dataset, suggestions); - } - }); + _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) { + var that = this, fragment; + fragment = document.createDocumentFragment(); + _.each(suggestions, function getSuggestionNode(suggestion) { + var $el, context; + context = that._injectQuery(query, suggestion); + $el = $(that.templates.suggestion(context)).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable); + fragment.appendChild($el[0]); }); + this.highlight && highlight({ + className: this.classes.highlight, + node: fragment, + pattern: query + }); + return $(fragment); }, - _autocomplete: function(e) { - var isCursorAtEnd, ignoreEvent, query, hint, suggestion; - if (e.type === "rightKeyed" || e.type === "leftKeyed") { - isCursorAtEnd = this.inputView.isCursorAtEnd(); - ignoreEvent = this.inputView.getLanguageDirection() === "ltr" ? e.type === "leftKeyed" : e.type === "rightKeyed"; - if (!isCursorAtEnd || ignoreEvent) { + _getFooter: function getFooter(query, suggestions) { + return this.templates.footer ? this.templates.footer({ + query: query, + suggestions: suggestions, + dataset: this.name + }) : null; + }, + _getHeader: function getHeader(query, suggestions) { + return this.templates.header ? this.templates.header({ + query: query, + suggestions: suggestions, + dataset: this.name + }) : null; + }, + _resetLastSuggestion: function resetLastSuggestion() { + this.$lastSuggestion = $(); + }, + _injectQuery: function injectQuery(query, obj) { + return _.isObject(obj) ? _.mixin({ + _query: query + }, obj) : obj; + }, + update: function update(query) { + var that = this, canceled = false, syncCalled = false, rendered = 0; + this.cancel(); + this.cancel = function cancel() { + canceled = true; + that.cancel = $.noop; + that.async && that.trigger("asyncCanceled", query); + }; + this.source(query, sync, async); + !syncCalled && sync([]); + function sync(suggestions) { + if (syncCalled) { return; } + syncCalled = true; + suggestions = (suggestions || []).slice(0, that.limit); + rendered = suggestions.length; + that._overwrite(query, suggestions); + if (rendered < that.limit && that.async) { + that.trigger("asyncRequested", query); + } } - query = this.inputView.getQuery(); - hint = this.inputView.getHintValue(); - if (hint !== "" && query !== hint) { - suggestion = this.dropdownView.getFirstSuggestion(); - this.inputView.setInputValue(suggestion.value); - this.eventBus.trigger("autocompleted", suggestion.datum, suggestion.dataset); + function async(suggestions) { + suggestions = suggestions || []; + if (!canceled && rendered < that.limit) { + that.cancel = $.noop; + rendered += suggestions.length; + that._append(query, suggestions.slice(0, that.limit - rendered)); + that.async && that.trigger("asyncReceived", query); + } } }, - _propagateEvent: function(e) { - this.eventBus.trigger(e.type); + cancel: $.noop, + clear: function clear() { + this._empty(); + this.cancel(); + this.trigger("cleared"); }, - destroy: function() { - this.inputView.destroy(); - this.dropdownView.destroy(); - destroyDomStructure(this.$node); - this.$node = null; + isEmpty: function isEmpty() { + return this.$el.is(":empty"); }, - setQuery: function(query) { - this.inputView.setQuery(query); - this.inputView.setInputValue(query); - this._clearHint(); - this._clearSuggestions(); - this._getSuggestions(); + destroy: function destroy() { + this.$el = $("
    "); } }); - return TypeaheadView; - function buildDomStructure(input) { - var $wrapper = $(html.wrapper), $dropdown = $(html.dropdown), $input = $(input), $hint = $(html.hint); - $wrapper = $wrapper.css(css.wrapper); - $dropdown = $dropdown.css(css.dropdown); - $hint.css(css.hint).css({ - backgroundAttachment: $input.css("background-attachment"), - backgroundClip: $input.css("background-clip"), - backgroundColor: $input.css("background-color"), - backgroundImage: $input.css("background-image"), - backgroundOrigin: $input.css("background-origin"), - backgroundPosition: $input.css("background-position"), - backgroundRepeat: $input.css("background-repeat"), - backgroundSize: $input.css("background-size") - }); - $input.data("ttAttrs", { - dir: $input.attr("dir"), - autocomplete: $input.attr("autocomplete"), - spellcheck: $input.attr("spellcheck"), - style: $input.attr("style") - }); - $input.addClass("tt-query").attr({ - autocomplete: "off", - spellcheck: false - }).css(css.query); - try { - !$input.attr("dir") && $input.attr("dir", "auto"); - } catch (e) {} - return $input.wrap($wrapper).parent().prepend($hint).append($dropdown); + return Dataset; + function getDisplayFn(display) { + display = display || _.stringify; + return _.isFunction(display) ? display : displayFn; + function displayFn(obj) { + return obj[display]; + } } - function destroyDomStructure($node) { - var $input = $node.find(".tt-query"); - utils.each($input.data("ttAttrs"), function(key, val) { - utils.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); - }); - $input.detach().removeData("ttAttrs").removeClass("tt-query").insertAfter($node); - $node.remove(); + function getTemplates(templates, displayFn) { + return { + notFound: templates.notFound && _.templatify(templates.notFound), + pending: templates.pending && _.templatify(templates.pending), + header: templates.header && _.templatify(templates.header), + footer: templates.footer && _.templatify(templates.footer), + suggestion: templates.suggestion || suggestionTemplate + }; + function suggestionTemplate(context) { + return $("
    ").text(displayFn(context)); + } + } + function isValidName(str) { + return /^[_a-zA-Z0-9-]+$/.test(str); + } + }(); + var Menu = function() { + "use strict"; + function Menu(o, www) { + var that = this; + o = o || {}; + if (!o.node) { + $.error("node is required"); + } + www.mixin(this); + this.$node = $(o.node); + this.query = null; + this.datasets = _.map(o.datasets, initializeDataset); + function initializeDataset(oDataset) { + var node = that.$node.find(oDataset.node).first(); + oDataset.node = node.length ? node : $("
    ").appendTo(that.$node); + return new Dataset(oDataset, www); + } + } + _.mixin(Menu.prototype, EventEmitter, { + _onSelectableClick: function onSelectableClick($e) { + this.trigger("selectableClicked", $($e.currentTarget)); + }, + _onRendered: function onRendered(type, dataset, suggestions, async) { + this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); + this.trigger("datasetRendered", dataset, suggestions, async); + }, + _onCleared: function onCleared() { + this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); + this.trigger("datasetCleared"); + }, + _propagate: function propagate() { + this.trigger.apply(this, arguments); + }, + _allDatasetsEmpty: function allDatasetsEmpty() { + return _.every(this.datasets, isDatasetEmpty); + function isDatasetEmpty(dataset) { + return dataset.isEmpty(); + } + }, + _getSelectables: function getSelectables() { + return this.$node.find(this.selectors.selectable); + }, + _removeCursor: function _removeCursor() { + var $selectable = this.getActiveSelectable(); + $selectable && $selectable.removeClass(this.classes.cursor); + }, + _ensureVisible: function ensureVisible($el) { + var elTop, elBottom, nodeScrollTop, nodeHeight; + elTop = $el.position().top; + elBottom = elTop + $el.outerHeight(true); + nodeScrollTop = this.$node.scrollTop(); + nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10); + if (elTop < 0) { + this.$node.scrollTop(nodeScrollTop + elTop); + } else if (nodeHeight < elBottom) { + this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight)); + } + }, + bind: function() { + var that = this, onSelectableClick; + onSelectableClick = _.bind(this._onSelectableClick, this); + this.$node.on("click.tt", this.selectors.selectable, onSelectableClick); + _.each(this.datasets, function(dataset) { + dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that); + }); + return this; + }, + isOpen: function isOpen() { + return this.$node.hasClass(this.classes.open); + }, + open: function open() { + this.$node.addClass(this.classes.open); + }, + close: function close() { + this.$node.removeClass(this.classes.open); + this._removeCursor(); + }, + setLanguageDirection: function setLanguageDirection(dir) { + this.$node.attr("dir", dir); + }, + selectableRelativeToCursor: function selectableRelativeToCursor(delta) { + var $selectables, $oldCursor, oldIndex, newIndex; + $oldCursor = this.getActiveSelectable(); + $selectables = this._getSelectables(); + oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1; + newIndex = oldIndex + delta; + newIndex = (newIndex + 1) % ($selectables.length + 1) - 1; + newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex; + return newIndex === -1 ? null : $selectables.eq(newIndex); + }, + setCursor: function setCursor($selectable) { + this._removeCursor(); + if ($selectable = $selectable && $selectable.first()) { + $selectable.addClass(this.classes.cursor); + this._ensureVisible($selectable); + } + }, + getSelectableData: function getSelectableData($el) { + return $el && $el.length ? Dataset.extractData($el) : null; + }, + getActiveSelectable: function getActiveSelectable() { + var $selectable = this._getSelectables().filter(this.selectors.cursor).first(); + return $selectable.length ? $selectable : null; + }, + getTopSelectable: function getTopSelectable() { + var $selectable = this._getSelectables().first(); + return $selectable.length ? $selectable : null; + }, + update: function update(query) { + var isValidUpdate = query !== this.query; + if (isValidUpdate) { + this.query = query; + _.each(this.datasets, updateDataset); + } + return isValidUpdate; + function updateDataset(dataset) { + dataset.update(query); + } + }, + empty: function empty() { + _.each(this.datasets, clearDataset); + this.query = null; + this.$node.addClass(this.classes.empty); + function clearDataset(dataset) { + dataset.clear(); + } + }, + destroy: function destroy() { + this.$node.off(".tt"); + this.$node = $("
    "); + _.each(this.datasets, destroyDataset); + function destroyDataset(dataset) { + dataset.destroy(); + } + } + }); + return Menu; + }(); + var DefaultMenu = function() { + "use strict"; + var s = Menu.prototype; + function DefaultMenu() { + Menu.apply(this, [].slice.call(arguments, 0)); + } + _.mixin(DefaultMenu.prototype, Menu.prototype, { + open: function open() { + !this._allDatasetsEmpty() && this._show(); + return s.open.apply(this, [].slice.call(arguments, 0)); + }, + close: function close() { + this._hide(); + return s.close.apply(this, [].slice.call(arguments, 0)); + }, + _onRendered: function onRendered() { + if (this._allDatasetsEmpty()) { + this._hide(); + } else { + this.isOpen() && this._show(); + } + return s._onRendered.apply(this, [].slice.call(arguments, 0)); + }, + _onCleared: function onCleared() { + if (this._allDatasetsEmpty()) { + this._hide(); + } else { + this.isOpen() && this._show(); + } + return s._onCleared.apply(this, [].slice.call(arguments, 0)); + }, + setLanguageDirection: function setLanguageDirection(dir) { + this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl); + return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0)); + }, + _hide: function hide() { + this.$node.hide(); + }, + _show: function show() { + this.$node.css("display", "block"); + } + }); + return DefaultMenu; + }(); + var Typeahead = function() { + "use strict"; + function Typeahead(o, www) { + var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged; + o = o || {}; + if (!o.input) { + $.error("missing input"); + } + if (!o.menu) { + $.error("missing menu"); + } + if (!o.eventBus) { + $.error("missing event bus"); + } + www.mixin(this); + this.eventBus = o.eventBus; + this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; + this.input = o.input; + this.menu = o.menu; + this.enabled = true; + this.active = false; + this.input.hasFocus() && this.activate(); + this.dir = this.input.getLangDir(); + this._hacks(); + this.menu.bind().onSync("selectableClicked", this._onSelectableClicked, this).onSync("asyncRequested", this._onAsyncRequested, this).onSync("asyncCanceled", this._onAsyncCanceled, this).onSync("asyncReceived", this._onAsyncReceived, this).onSync("datasetRendered", this._onDatasetRendered, this).onSync("datasetCleared", this._onDatasetCleared, this); + onFocused = c(this, "activate", "open", "_onFocused"); + onBlurred = c(this, "deactivate", "_onBlurred"); + onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed"); + onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed"); + onEscKeyed = c(this, "isActive", "_onEscKeyed"); + onUpKeyed = c(this, "isActive", "open", "_onUpKeyed"); + onDownKeyed = c(this, "isActive", "open", "_onDownKeyed"); + onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed"); + onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed"); + onQueryChanged = c(this, "_openIfActive", "_onQueryChanged"); + onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged"); + this.input.bind().onSync("focused", onFocused, this).onSync("blurred", onBlurred, this).onSync("enterKeyed", onEnterKeyed, this).onSync("tabKeyed", onTabKeyed, this).onSync("escKeyed", onEscKeyed, this).onSync("upKeyed", onUpKeyed, this).onSync("downKeyed", onDownKeyed, this).onSync("leftKeyed", onLeftKeyed, this).onSync("rightKeyed", onRightKeyed, this).onSync("queryChanged", onQueryChanged, this).onSync("whitespaceChanged", onWhitespaceChanged, this).onSync("langDirChanged", this._onLangDirChanged, this); + } + _.mixin(Typeahead.prototype, { + _hacks: function hacks() { + var $input, $menu; + $input = this.input.$input || $("
    "); + $menu = this.menu.$node || $("
    "); + $input.on("blur.tt", function($e) { + var active, isActive, hasActive; + active = document.activeElement; + isActive = $menu.is(active); + hasActive = $menu.has(active).length > 0; + if (_.isMsie() && (isActive || hasActive)) { + $e.preventDefault(); + $e.stopImmediatePropagation(); + _.defer(function() { + $input.focus(); + }); + } + }); + $menu.on("mousedown.tt", function($e) { + $e.preventDefault(); + }); + }, + _onSelectableClicked: function onSelectableClicked(type, $el) { + this.select($el); + }, + _onDatasetCleared: function onDatasetCleared() { + this._updateHint(); + }, + _onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) { + this._updateHint(); + this.eventBus.trigger("render", suggestions, async, dataset); + }, + _onAsyncRequested: function onAsyncRequested(type, dataset, query) { + this.eventBus.trigger("asyncrequest", query, dataset); + }, + _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) { + this.eventBus.trigger("asynccancel", query, dataset); + }, + _onAsyncReceived: function onAsyncReceived(type, dataset, query) { + this.eventBus.trigger("asyncreceive", query, dataset); + }, + _onFocused: function onFocused() { + this._minLengthMet() && this.menu.update(this.input.getQuery()); + }, + _onBlurred: function onBlurred() { + if (this.input.hasQueryChangedSinceLastFocus()) { + this.eventBus.trigger("change", this.input.getQuery()); + } + }, + _onEnterKeyed: function onEnterKeyed(type, $e) { + var $selectable; + if ($selectable = this.menu.getActiveSelectable()) { + this.select($selectable) && $e.preventDefault(); + } + }, + _onTabKeyed: function onTabKeyed(type, $e) { + var $selectable; + if ($selectable = this.menu.getActiveSelectable()) { + this.select($selectable) && $e.preventDefault(); + } else if ($selectable = this.menu.getTopSelectable()) { + this.autocomplete($selectable) && $e.preventDefault(); + } + }, + _onEscKeyed: function onEscKeyed() { + this.close(); + }, + _onUpKeyed: function onUpKeyed() { + this.moveCursor(-1); + }, + _onDownKeyed: function onDownKeyed() { + this.moveCursor(+1); + }, + _onLeftKeyed: function onLeftKeyed() { + if (this.dir === "rtl" && this.input.isCursorAtEnd()) { + this.autocomplete(this.menu.getTopSelectable()); + } + }, + _onRightKeyed: function onRightKeyed() { + if (this.dir === "ltr" && this.input.isCursorAtEnd()) { + this.autocomplete(this.menu.getTopSelectable()); + } + }, + _onQueryChanged: function onQueryChanged(e, query) { + this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty(); + }, + _onWhitespaceChanged: function onWhitespaceChanged() { + this._updateHint(); + }, + _onLangDirChanged: function onLangDirChanged(e, dir) { + if (this.dir !== dir) { + this.dir = dir; + this.menu.setLanguageDirection(dir); + } + }, + _openIfActive: function openIfActive() { + this.isActive() && this.open(); + }, + _minLengthMet: function minLengthMet(query) { + query = _.isString(query) ? query : this.input.getQuery() || ""; + return query.length >= this.minLength; + }, + _updateHint: function updateHint() { + var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match; + $selectable = this.menu.getTopSelectable(); + data = this.menu.getSelectableData($selectable); + val = this.input.getInputValue(); + if (data && !_.isBlankString(val) && !this.input.hasOverflow()) { + query = Input.normalizeQuery(val); + escapedQuery = _.escapeRegExChars(query); + frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i"); + match = frontMatchRegEx.exec(data.val); + match && this.input.setHint(val + match[1]); + } else { + this.input.clearHint(); + } + }, + isEnabled: function isEnabled() { + return this.enabled; + }, + enable: function enable() { + this.enabled = true; + }, + disable: function disable() { + this.enabled = false; + }, + isActive: function isActive() { + return this.active; + }, + activate: function activate() { + if (this.isActive()) { + return true; + } else if (!this.isEnabled() || this.eventBus.before("active")) { + return false; + } else { + this.active = true; + this.eventBus.trigger("active"); + return true; + } + }, + deactivate: function deactivate() { + if (!this.isActive()) { + return true; + } else if (this.eventBus.before("idle")) { + return false; + } else { + this.active = false; + this.close(); + this.eventBus.trigger("idle"); + return true; + } + }, + isOpen: function isOpen() { + return this.menu.isOpen(); + }, + open: function open() { + if (!this.isOpen() && !this.eventBus.before("open")) { + this.menu.open(); + this._updateHint(); + this.eventBus.trigger("open"); + } + return this.isOpen(); + }, + close: function close() { + if (this.isOpen() && !this.eventBus.before("close")) { + this.menu.close(); + this.input.clearHint(); + this.input.resetInputValue(); + this.eventBus.trigger("close"); + } + return !this.isOpen(); + }, + setVal: function setVal(val) { + this.input.setQuery(_.toStr(val)); + }, + getVal: function getVal() { + return this.input.getQuery(); + }, + select: function select($selectable) { + var data = this.menu.getSelectableData($selectable); + if (data && !this.eventBus.before("select", data.obj)) { + this.input.setQuery(data.val, true); + this.eventBus.trigger("select", data.obj); + this.close(); + return true; + } + return false; + }, + autocomplete: function autocomplete($selectable) { + var query, data, isValid; + query = this.input.getQuery(); + data = this.menu.getSelectableData($selectable); + isValid = data && query !== data.val; + if (isValid && !this.eventBus.before("autocomplete", data.obj)) { + this.input.setQuery(data.val); + this.eventBus.trigger("autocomplete", data.obj); + return true; + } + return false; + }, + moveCursor: function moveCursor(delta) { + var query, $candidate, data, payload, cancelMove; + query = this.input.getQuery(); + $candidate = this.menu.selectableRelativeToCursor(delta); + data = this.menu.getSelectableData($candidate); + payload = data ? data.obj : null; + cancelMove = this._minLengthMet() && this.menu.update(query); + if (!cancelMove && !this.eventBus.before("cursorchange", payload)) { + this.menu.setCursor($candidate); + if (data) { + this.input.setInputValue(data.val); + } else { + this.input.resetInputValue(); + this._updateHint(); + } + this.eventBus.trigger("cursorchange", payload); + return true; + } + return false; + }, + destroy: function destroy() { + this.input.destroy(); + this.menu.destroy(); + } + }); + return Typeahead; + function c(ctx) { + var methods = [].slice.call(arguments, 1); + return function() { + var args = [].slice.call(arguments); + _.each(methods, function(method) { + return ctx[method].apply(ctx, args); + }); + }; } }(); (function() { - var cache = {}, viewKey = "ttView", methods; + "use strict"; + var old, keys, methods; + old = $.fn.typeahead; + keys = { + www: "tt-www", + attrs: "tt-attrs", + typeahead: "tt-typeahead" + }; methods = { - initialize: function(datasetDefs) { - var datasets; - datasetDefs = utils.isArray(datasetDefs) ? datasetDefs : [ datasetDefs ]; - if (datasetDefs.length === 0) { - $.error("no datasets provided"); - } - datasets = utils.map(datasetDefs, function(o) { - var dataset = cache[o.name] ? cache[o.name] : new Dataset(o); - if (o.name) { - cache[o.name] = dataset; + initialize: function initialize(o, datasets) { + var www; + datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); + o = o || {}; + www = WWW(o.classNames); + return this.each(attach); + function attach() { + var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, typeahead, MenuConstructor; + _.each(datasets, function(d) { + d.highlight = !!o.highlight; + }); + $input = $(this); + $wrapper = $(www.html.wrapper); + $hint = $elOrNull(o.hint); + $menu = $elOrNull(o.menu); + defaultHint = o.hint !== false && !$hint; + defaultMenu = o.menu !== false && !$menu; + defaultHint && ($hint = buildHintFromInput($input, www)); + defaultMenu && ($menu = $(www.html.menu).css(www.css.menu)); + $hint && $hint.val(""); + $input = prepInput($input, www); + if (defaultHint || defaultMenu) { + $wrapper.css(www.css.wrapper); + $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint); + $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null); } - return dataset; - }); - return this.each(initialize); - function initialize() { - var $input = $(this), deferreds, eventBus = new EventBus({ + MenuConstructor = defaultMenu ? DefaultMenu : Menu; + eventBus = new EventBus({ el: $input }); - deferreds = utils.map(datasets, function(dataset) { - return dataset.initialize(); - }); - $input.data(viewKey, new TypeaheadView({ - input: $input, - eventBus: eventBus = new EventBus({ - el: $input - }), + input = new Input({ + hint: $hint, + input: $input + }, www); + menu = new MenuConstructor({ + node: $menu, datasets: datasets - })); - $.when.apply($, deferreds).always(function() { - utils.defer(function() { - eventBus.trigger("initialized"); - }); + }, www); + typeahead = new Typeahead({ + input: input, + menu: menu, + eventBus: eventBus, + minLength: o.minLength + }, www); + $input.data(keys.www, www); + $input.data(keys.typeahead, typeahead); + } + }, + isEnabled: function isEnabled() { + var enabled; + ttEach(this.first(), function(t) { + enabled = t.isEnabled(); + }); + return enabled; + }, + enable: function enable() { + ttEach(this, function(t) { + t.enable(); + }); + return this; + }, + disable: function disable() { + ttEach(this, function(t) { + t.disable(); + }); + return this; + }, + isActive: function isActive() { + var active; + ttEach(this.first(), function(t) { + active = t.isActive(); + }); + return active; + }, + activate: function activate() { + ttEach(this, function(t) { + t.activate(); + }); + return this; + }, + deactivate: function deactivate() { + ttEach(this, function(t) { + t.deactivate(); + }); + return this; + }, + isOpen: function isOpen() { + var open; + ttEach(this.first(), function(t) { + open = t.isOpen(); + }); + return open; + }, + open: function open() { + ttEach(this, function(t) { + t.open(); + }); + return this; + }, + close: function close() { + ttEach(this, function(t) { + t.close(); + }); + return this; + }, + select: function select(el) { + var success = false, $el = $(el); + ttEach(this.first(), function(t) { + success = t.select($el); + }); + return success; + }, + autocomplete: function autocomplete(el) { + var success = false, $el = $(el); + ttEach(this.first(), function(t) { + success = t.autocomplete($el); + }); + return success; + }, + moveCursor: function moveCursoe(delta) { + var success = false; + ttEach(this.first(), function(t) { + success = t.moveCursor(delta); + }); + return success; + }, + val: function val(newVal) { + var query; + if (!arguments.length) { + ttEach(this.first(), function(t) { + query = t.getVal(); }); + return query; + } else { + ttEach(this, function(t) { + t.setVal(newVal); + }); + return this; } }, - destroy: function() { - return this.each(destroy); - function destroy() { - var $this = $(this), view = $this.data(viewKey); - if (view) { - view.destroy(); - $this.removeData(viewKey); - } - } - }, - setQuery: function(query) { - return this.each(setQuery); - function setQuery() { - var view = $(this).data(viewKey); - view && view.setQuery(query); - } + destroy: function destroy() { + ttEach(this, function(typeahead, $input) { + revert($input); + typeahead.destroy(); + }); + return this; } }; - jQuery.fn.typeahead = function(method) { + $.fn.typeahead = function(method) { if (methods[method]) { return methods[method].apply(this, [].slice.call(arguments, 1)); } else { return methods.initialize.apply(this, arguments); } }; + $.fn.typeahead.noConflict = function noConflict() { + $.fn.typeahead = old; + return this; + }; + function ttEach($els, fn) { + $els.each(function() { + var $input = $(this), typeahead; + (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input); + }); + } + function buildHintFromInput($input, www) { + return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop("readonly", true).removeAttr("id name placeholder required").attr({ + autocomplete: "off", + spellcheck: "false", + tabindex: -1 + }); + } + function prepInput($input, www) { + $input.data(keys.attrs, { + dir: $input.attr("dir"), + autocomplete: $input.attr("autocomplete"), + spellcheck: $input.attr("spellcheck"), + style: $input.attr("style") + }); + $input.addClass(www.classes.input).attr({ + autocomplete: "off", + spellcheck: false + }); + try { + !$input.attr("dir") && $input.attr("dir", "auto"); + } catch (e) {} + return $input; + } + function getBackgroundStyles($el) { + return { + backgroundAttachment: $el.css("background-attachment"), + backgroundClip: $el.css("background-clip"), + backgroundColor: $el.css("background-color"), + backgroundImage: $el.css("background-image"), + backgroundOrigin: $el.css("background-origin"), + backgroundPosition: $el.css("background-position"), + backgroundRepeat: $el.css("background-repeat"), + backgroundSize: $el.css("background-size") + }; + } + function revert($input) { + var www, $wrapper; + www = $input.data(keys.www); + $wrapper = $input.parent().filter(www.selectors.wrapper); + _.each($input.data(keys.attrs), function(val, key) { + _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); + }); + $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input); + if ($wrapper.length) { + $input.detach().insertAfter($wrapper); + $wrapper.remove(); + } + } + function $elOrNull(obj) { + var isValid, $el; + isValid = _.isJQuery(obj) || _.isElement(obj); + $el = isValid ? $(obj).first() : []; + return $el.length ? $el : null; + } })(); -})(window.jQuery); +}); \ No newline at end of file diff --git a/app/assets/javascripts/src/Metamaps.js b/app/assets/javascripts/src/Metamaps.js index af0518d0..5792eb96 100644 --- a/app/assets/javascripts/src/Metamaps.js +++ b/app/assets/javascripts/src/Metamaps.js @@ -669,7 +669,7 @@ Metamaps.Create = { { minLength: 2, }, - [{ + { name: 'topic_autocomplete', limit: 8, template: $('#topicAutocompleteTemplate').html(), @@ -677,7 +677,7 @@ Metamaps.Create = { url: '/topics/autocomplete_topic?term=%QUERY' }, engine: Hogan - }] + } ); // tell the autocomplete to submit the form with the topic you clicked on if you pick from the autocomplete @@ -731,7 +731,7 @@ Metamaps.Create = { { minLength: 2, }, - [{ + { name: 'synapse_autocomplete', template: "
    {{label}}
    ", remote: { @@ -751,7 +751,7 @@ Metamaps.Create = { }, engine: Hogan, header: "

    Existing synapses

    " - }] + } ); $('#synapse_desc').bind('typeahead:selected', function (event, datum, dataset) { From ff48b2456ad5e2696ca6b31b899109b62e8143a2 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 1 Oct 2015 11:34:19 +0800 Subject: [PATCH 027/305] Revert "upgrade typeahead to allow new syntax" This reverts commit 347d77df824362b14770eede6cc3ab21ee4d6f07. --- app/assets/javascripts/lib/typeahead.js | 2453 ++++++++++------------- app/assets/javascripts/src/Metamaps.js | 8 +- 2 files changed, 1044 insertions(+), 1417 deletions(-) diff --git a/app/assets/javascripts/lib/typeahead.js b/app/assets/javascripts/lib/typeahead.js index 2b089289..3a413d68 100644 --- a/app/assets/javascripts/lib/typeahead.js +++ b/app/assets/javascripts/lib/typeahead.js @@ -1,608 +1,659 @@ /*! - * typeahead.js 0.11.1 - * https://github.com/twitter/typeahead.js - * Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT + * typeahead.js 0.9.3 + * https://github.com/twitter/typeahead + * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT */ -(function(root, factory) { - if (typeof define === "function" && define.amd) { - define("typeahead.js", [ "jquery" ], function(a0) { - return factory(a0); - }); - } else if (typeof exports === "object") { - module.exports = factory(require("jquery")); - } else { - factory(jQuery); - } -})(this, function($) { - var _ = function() { - "use strict"; - return { - isMsie: function() { - return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; - }, - isBlankString: function(str) { - return !str || /^\s*$/.test(str); - }, - escapeRegExChars: function(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }, - isString: function(obj) { - return typeof obj === "string"; - }, - isNumber: function(obj) { - return typeof obj === "number"; - }, - isArray: $.isArray, - isFunction: $.isFunction, - isObject: $.isPlainObject, - isUndefined: function(obj) { - return typeof obj === "undefined"; - }, - isElement: function(obj) { - return !!(obj && obj.nodeType === 1); - }, - isJQuery: function(obj) { - return obj instanceof $; - }, - toStr: function toStr(s) { - return _.isUndefined(s) || s === null ? "" : s + ""; - }, - bind: $.proxy, - each: function(collection, cb) { - $.each(collection, reverseArgs); - function reverseArgs(index, value) { - return cb(value, index); - } - }, - map: $.map, - filter: $.grep, - every: function(obj, test) { - var result = true; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (!(result = test.call(null, val, key, obj))) { - return false; - } - }); - return !!result; - }, - some: function(obj, test) { - var result = false; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (result = test.call(null, val, key, obj)) { - return false; - } - }); - return !!result; - }, - mixin: $.extend, - identity: function(x) { - return x; - }, - clone: function(obj) { - return $.extend(true, {}, obj); - }, - getIdGenerator: function() { - var counter = 0; - return function() { - return counter++; - }; - }, - templatify: function templatify(obj) { - return $.isFunction(obj) ? obj : template; - function template() { - return String(obj); - } - }, - defer: function(fn) { - setTimeout(fn, 0); - }, - debounce: function(func, wait, immediate) { - var timeout, result; - return function() { - var context = this, args = arguments, later, callNow; - later = function() { - timeout = null; - if (!immediate) { - result = func.apply(context, args); - } - }; - callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - result = func.apply(context, args); - } - return result; - }; - }, - throttle: function(func, wait) { - var context, args, timeout, result, previous, later; - previous = 0; - later = function() { - previous = new Date(); - timeout = null; - result = func.apply(context, args); - }; - return function() { - var now = new Date(), remaining = wait - (now - previous); - context = this; - args = arguments; - if (remaining <= 0) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(context, args); - } else if (!timeout) { - timeout = setTimeout(later, remaining); - } - return result; - }; - }, - stringify: function(val) { - return _.isString(val) ? val : JSON.stringify(val); - }, - noop: function() {} - }; - }(); - var WWW = function() { - "use strict"; - var defaultClassNames = { - wrapper: "twitter-typeahead", - input: "tt-input", - hint: "tt-hint", - menu: "tt-menu", - dataset: "tt-dataset", - suggestion: "tt-suggestion", - selectable: "tt-selectable", - empty: "tt-empty", - open: "tt-open", - cursor: "tt-cursor", - highlight: "tt-highlight" - }; - return build; - function build(o) { - var www, classes; - classes = _.mixin({}, defaultClassNames, o); - www = { - css: buildCss(), - classes: classes, - html: buildHtml(classes), - selectors: buildSelectors(classes) - }; - return { - css: www.css, - html: www.html, - classes: www.classes, - selectors: www.selectors, - mixin: function(o) { - _.mixin(o, www); - } - }; - } - function buildHtml(c) { - return { - wrapper: '', - menu: '
    ' - }; - } - function buildSelectors(classes) { - var selectors = {}; - _.each(classes, function(v, k) { - selectors[k] = "." + v; - }); - return selectors; - } - function buildCss() { - var css = { - wrapper: { - position: "relative", - display: "inline-block" - }, - hint: { - position: "absolute", - top: "0", - left: "0", - borderColor: "transparent", - boxShadow: "none", - opacity: "1" - }, - input: { - position: "relative", - verticalAlign: "top", - backgroundColor: "transparent" - }, - inputWithNoHint: { - position: "relative", - verticalAlign: "top" - }, - menu: { - position: "absolute", - top: "100%", - left: "0", - zIndex: "100", - display: "none" - }, - ltr: { - left: "0", - right: "auto" - }, - rtl: { - left: "auto", - right: " 0" - } - }; - if (_.isMsie()) { - _.mixin(css.input, { - backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" - }); +(function($) { + var VERSION = "0.9.3"; + var utils = { + isMsie: function() { + var match = /(msie) ([\w.]+)/i.exec(navigator.userAgent); + return match ? parseInt(match[2], 10) : false; + }, + isBlankString: function(str) { + return !str || /^\s*$/.test(str); + }, + escapeRegExChars: function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }, + isString: function(obj) { + return typeof obj === "string"; + }, + isNumber: function(obj) { + return typeof obj === "number"; + }, + isArray: $.isArray, + isFunction: $.isFunction, + isObject: $.isPlainObject, + isUndefined: function(obj) { + return typeof obj === "undefined"; + }, + bind: $.proxy, + bindAll: function(obj) { + var val; + for (var key in obj) { + $.isFunction(val = obj[key]) && (obj[key] = $.proxy(val, obj)); } - return css; - } + }, + indexOf: function(haystack, needle) { + for (var i = 0; i < haystack.length; i++) { + if (haystack[i] === needle) { + return i; + } + } + return -1; + }, + each: $.each, + map: $.map, + filter: $.grep, + every: function(obj, test) { + var result = true; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (!(result = test.call(null, val, key, obj))) { + return false; + } + }); + return !!result; + }, + some: function(obj, test) { + var result = false; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (result = test.call(null, val, key, obj)) { + return false; + } + }); + return !!result; + }, + mixin: $.extend, + getUniqueId: function() { + var counter = 0; + return function() { + return counter++; + }; + }(), + defer: function(fn) { + setTimeout(fn, 0); + }, + debounce: function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments, later, callNow; + later = function() { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + } + }; + callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + } + return result; + }; + }, + throttle: function(func, wait) { + var context, args, timeout, result, previous, later; + previous = 0; + later = function() { + previous = new Date(); + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = new Date(), remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }, + tokenizeQuery: function(str) { + return $.trim(str).toLowerCase().split(/[\s]+/); + }, + tokenizeText: function(str) { + return $.trim(str).toLowerCase().split(/[\s\-_]+/); + }, + getProtocol: function() { + return location.protocol; + }, + noop: function() {} + }; + var EventTarget = function() { + var eventSplitter = /\s+/; + return { + on: function(events, callback) { + var event; + if (!callback) { + return this; + } + this._callbacks = this._callbacks || {}; + events = events.split(eventSplitter); + while (event = events.shift()) { + this._callbacks[event] = this._callbacks[event] || []; + this._callbacks[event].push(callback); + } + return this; + }, + trigger: function(events, data) { + var event, callbacks; + if (!this._callbacks) { + return this; + } + events = events.split(eventSplitter); + while (event = events.shift()) { + if (callbacks = this._callbacks[event]) { + for (var i = 0; i < callbacks.length; i += 1) { + callbacks[i].call(this, { + type: event, + data: data + }); + } + } + } + return this; + } + }; }(); var EventBus = function() { - "use strict"; - var namespace, deprecationMap; - namespace = "typeahead:"; - deprecationMap = { - render: "rendered", - cursorchange: "cursorchanged", - select: "selected", - autocomplete: "autocompleted" - }; + var namespace = "typeahead:"; function EventBus(o) { if (!o || !o.el) { $.error("EventBus initialized without el"); } this.$el = $(o.el); } - _.mixin(EventBus.prototype, { - _trigger: function(type, args) { - var $e; - $e = $.Event(namespace + type); - (args = args || []).unshift($e); - this.$el.trigger.apply(this.$el, args); - return $e; - }, - before: function(type) { - var args, $e; - args = [].slice.call(arguments, 1); - $e = this._trigger("before" + type, args); - return $e.isDefaultPrevented(); - }, + utils.mixin(EventBus.prototype, { trigger: function(type) { - var deprecatedType; - this._trigger(type, [].slice.call(arguments, 1)); - if (deprecatedType = deprecationMap[type]) { - this._trigger(deprecatedType, [].slice.call(arguments, 1)); - } + var args = [].slice.call(arguments, 1); + this.$el.trigger(namespace + type, args); } }); return EventBus; }(); - var EventEmitter = function() { - "use strict"; - var splitter = /\s+/, nextTick = getNextTick(); - return { - onSync: onSync, - onAsync: onAsync, - off: off, - trigger: trigger - }; - function on(method, types, cb, context) { - var type; - if (!cb) { - return this; - } - types = types.split(splitter); - cb = context ? bindContext(cb, context) : cb; - this._callbacks = this._callbacks || {}; - while (type = types.shift()) { - this._callbacks[type] = this._callbacks[type] || { - sync: [], - async: [] - }; - this._callbacks[type][method].push(cb); - } - return this; + var PersistentStorage = function() { + var ls, methods; + try { + ls = window.localStorage; + ls.setItem("~~~", "!"); + ls.removeItem("~~~"); + } catch (err) { + ls = null; } - function onAsync(types, cb, context) { - return on.call(this, "async", types, cb, context); + function PersistentStorage(namespace) { + this.prefix = [ "__", namespace, "__" ].join(""); + this.ttlKey = "__ttl__"; + this.keyMatcher = new RegExp("^" + this.prefix); } - function onSync(types, cb, context) { - return on.call(this, "sync", types, cb, context); - } - function off(types) { - var type; - if (!this._callbacks) { - return this; - } - types = types.split(splitter); - while (type = types.shift()) { - delete this._callbacks[type]; - } - return this; - } - function trigger(types) { - var type, callbacks, args, syncFlush, asyncFlush; - if (!this._callbacks) { - return this; - } - types = types.split(splitter); - args = [].slice.call(arguments, 1); - while ((type = types.shift()) && (callbacks = this._callbacks[type])) { - syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args)); - asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args)); - syncFlush() && nextTick(asyncFlush); - } - return this; - } - function getFlush(callbacks, context, args) { - return flush; - function flush() { - var cancelled; - for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) { - cancelled = callbacks[i].apply(context, args) === false; + if (ls && window.JSON) { + methods = { + _prefix: function(key) { + return this.prefix + key; + }, + _ttlKey: function(key) { + return this._prefix(key) + this.ttlKey; + }, + get: function(key) { + if (this.isExpired(key)) { + this.remove(key); + } + return decode(ls.getItem(this._prefix(key))); + }, + set: function(key, val, ttl) { + if (utils.isNumber(ttl)) { + ls.setItem(this._ttlKey(key), encode(now() + ttl)); + } else { + ls.removeItem(this._ttlKey(key)); + } + return ls.setItem(this._prefix(key), encode(val)); + }, + remove: function(key) { + ls.removeItem(this._ttlKey(key)); + ls.removeItem(this._prefix(key)); + return this; + }, + clear: function() { + var i, key, keys = [], len = ls.length; + for (i = 0; i < len; i++) { + if ((key = ls.key(i)).match(this.keyMatcher)) { + keys.push(key.replace(this.keyMatcher, "")); + } + } + for (i = keys.length; i--; ) { + this.remove(keys[i]); + } + return this; + }, + isExpired: function(key) { + var ttl = decode(ls.getItem(this._ttlKey(key))); + return utils.isNumber(ttl) && now() > ttl ? true : false; } - return !cancelled; - } - } - function getNextTick() { - var nextTickFn; - if (window.setImmediate) { - nextTickFn = function nextTickSetImmediate(fn) { - setImmediate(function() { - fn(); - }); - }; - } else { - nextTickFn = function nextTickSetTimeout(fn) { - setTimeout(function() { - fn(); - }, 0); - }; - } - return nextTickFn; - } - function bindContext(fn, context) { - return fn.bind ? fn.bind(context) : function() { - fn.apply(context, [].slice.call(arguments, 0)); + }; + } else { + methods = { + get: utils.noop, + set: utils.noop, + remove: utils.noop, + clear: utils.noop, + isExpired: utils.noop }; } + utils.mixin(PersistentStorage.prototype, methods); + return PersistentStorage; + function now() { + return new Date().getTime(); + } + function encode(val) { + return JSON.stringify(utils.isUndefined(val) ? null : val); + } + function decode(val) { + return JSON.parse(val); + } }(); - var highlight = function(doc) { - "use strict"; - var defaults = { - node: null, - pattern: null, - tagName: "strong", - className: null, - wordsOnly: false, - caseSensitive: false - }; - return function hightlight(o) { - var regex; - o = _.mixin({}, defaults, o); - if (!o.node || !o.pattern) { - return; - } - o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; - regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); - traverse(o.node, hightlightTextNode); - function hightlightTextNode(textNode) { - var match, patternNode, wrapperNode; - if (match = regex.exec(textNode.data)) { - wrapperNode = doc.createElement(o.tagName); - o.className && (wrapperNode.className = o.className); - patternNode = textNode.splitText(match.index); - patternNode.splitText(match[0].length); - wrapperNode.appendChild(patternNode.cloneNode(true)); - textNode.parentNode.replaceChild(wrapperNode, patternNode); + var RequestCache = function() { + function RequestCache(o) { + utils.bindAll(this); + o = o || {}; + this.sizeLimit = o.sizeLimit || 10; + this.cache = {}; + this.cachedKeysByAge = []; + } + utils.mixin(RequestCache.prototype, { + get: function(url) { + return this.cache[url]; + }, + set: function(url, resp) { + var requestToEvict; + if (this.cachedKeysByAge.length === this.sizeLimit) { + requestToEvict = this.cachedKeysByAge.shift(); + delete this.cache[requestToEvict]; } - return !!match; + this.cache[url] = resp; + this.cachedKeysByAge.push(url); } - function traverse(el, hightlightTextNode) { - var childNode, TEXT_NODE_TYPE = 3; - for (var i = 0; i < el.childNodes.length; i++) { - childNode = el.childNodes[i]; - if (childNode.nodeType === TEXT_NODE_TYPE) { - i += hightlightTextNode(childNode) ? 1 : 0; - } else { - traverse(childNode, hightlightTextNode); + }); + return RequestCache; + }(); + var Transport = function() { + var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests, requestCache; + function Transport(o) { + utils.bindAll(this); + o = utils.isString(o) ? { + url: o + } : o; + requestCache = requestCache || new RequestCache(); + maxPendingRequests = utils.isNumber(o.maxParallelRequests) ? o.maxParallelRequests : maxPendingRequests || 6; + this.url = o.url; + this.wildcard = o.wildcard || "%QUERY"; + this.filter = o.filter; + this.replace = o.replace; + this.ajaxSettings = { + type: "get", + cache: o.cache, + timeout: o.timeout, + dataType: o.dataType || "json", + beforeSend: o.beforeSend + }; + this._get = (/^throttle$/i.test(o.rateLimitFn) ? utils.throttle : utils.debounce)(this._get, o.rateLimitWait || 300); + } + utils.mixin(Transport.prototype, { + _get: function(url, cb) { + var that = this; + if (belowPendingRequestsThreshold()) { + this._sendRequest(url).done(done); + } else { + this.onDeckRequestArgs = [].slice.call(arguments, 0); + } + function done(resp) { + var data = that.filter ? that.filter(resp) : resp; + cb && cb(data); + requestCache.set(url, resp); + } + }, + _sendRequest: function(url) { + var that = this, jqXhr = pendingRequests[url]; + if (!jqXhr) { + incrementPendingRequests(); + jqXhr = pendingRequests[url] = $.ajax(url, this.ajaxSettings).always(always); + } + return jqXhr; + function always() { + decrementPendingRequests(); + pendingRequests[url] = null; + if (that.onDeckRequestArgs) { + that._get.apply(that, that.onDeckRequestArgs); + that.onDeckRequestArgs = null; } } - } - }; - function getRegex(patterns, caseSensitive, wordsOnly) { - var escapedPatterns = [], regexStr; - for (var i = 0, len = patterns.length; i < len; i++) { - escapedPatterns.push(_.escapeRegExChars(patterns[i])); - } - regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; - return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); - } - }(window.document); - var Input = function() { - "use strict"; - var specialKeyCodeMap; - specialKeyCodeMap = { - 9: "tab", - 27: "esc", - 37: "left", - 39: "right", - 13: "enter", - 38: "up", - 40: "down" - }; - function Input(o, www) { - o = o || {}; - if (!o.input) { - $.error("input is missing"); - } - www.mixin(this); - this.$hint = $(o.hint); - this.$input = $(o.input); - this.query = this.$input.val(); - this.queryWhenFocused = this.hasFocus() ? this.query : null; - this.$overflowHelper = buildOverflowHelper(this.$input); - this._checkLanguageDirection(); - if (this.$hint.length === 0) { - this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; - } - } - Input.normalizeQuery = function(str) { - return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " "); - }; - _.mixin(Input.prototype, EventEmitter, { - _onBlur: function onBlur() { - this.resetInputValue(); - this.trigger("blurred"); }, - _onFocus: function onFocus() { - this.queryWhenFocused = this.query; + get: function(query, cb) { + var that = this, encodedQuery = encodeURIComponent(query || ""), url, resp; + cb = cb || utils.noop; + url = this.replace ? this.replace(this.url, encodedQuery) : this.url.replace(this.wildcard, encodedQuery); + if (resp = requestCache.get(url)) { + utils.defer(function() { + cb(that.filter ? that.filter(resp) : resp); + }); + } else { + this._get(url, cb); + } + return !!resp; + } + }); + return Transport; + function incrementPendingRequests() { + pendingRequestsCount++; + } + function decrementPendingRequests() { + pendingRequestsCount--; + } + function belowPendingRequestsThreshold() { + return pendingRequestsCount < maxPendingRequests; + } + }(); + var Dataset = function() { + var keys = { + thumbprint: "thumbprint", + protocol: "protocol", + itemHash: "itemHash", + adjacencyList: "adjacencyList" + }; + function Dataset(o) { + utils.bindAll(this); + if (utils.isString(o.template) && !o.engine) { + $.error("no template engine specified"); + } + if (!o.local && !o.prefetch && !o.remote) { + $.error("one of local, prefetch, or remote is required"); + } + this.name = o.name || utils.getUniqueId(); + this.limit = o.limit || 5; + this.minLength = o.minLength || 1; + this.header = o.header; + this.footer = o.footer; + this.valueKey = o.valueKey || "value"; + this.template = compileTemplate(o.template, o.engine, this.valueKey); + this.local = o.local; + this.prefetch = o.prefetch; + this.remote = o.remote; + this.itemHash = {}; + this.adjacencyList = {}; + this.storage = o.name ? new PersistentStorage(o.name) : null; + } + utils.mixin(Dataset.prototype, { + _processLocalData: function(data) { + this._mergeProcessedData(this._processData(data)); + }, + _loadPrefetchData: function(o) { + var that = this, thumbprint = VERSION + (o.thumbprint || ""), storedThumbprint, storedProtocol, storedItemHash, storedAdjacencyList, isExpired, deferred; + if (this.storage) { + storedThumbprint = this.storage.get(keys.thumbprint); + storedProtocol = this.storage.get(keys.protocol); + storedItemHash = this.storage.get(keys.itemHash); + storedAdjacencyList = this.storage.get(keys.adjacencyList); + } + isExpired = storedThumbprint !== thumbprint || storedProtocol !== utils.getProtocol(); + o = utils.isString(o) ? { + url: o + } : o; + o.ttl = utils.isNumber(o.ttl) ? o.ttl : 24 * 60 * 60 * 1e3; + if (storedItemHash && storedAdjacencyList && !isExpired) { + this._mergeProcessedData({ + itemHash: storedItemHash, + adjacencyList: storedAdjacencyList + }); + deferred = $.Deferred().resolve(); + } else { + deferred = $.getJSON(o.url).done(processPrefetchData); + } + return deferred; + function processPrefetchData(data) { + var filteredData = o.filter ? o.filter(data) : data, processedData = that._processData(filteredData), itemHash = processedData.itemHash, adjacencyList = processedData.adjacencyList; + if (that.storage) { + that.storage.set(keys.itemHash, itemHash, o.ttl); + that.storage.set(keys.adjacencyList, adjacencyList, o.ttl); + that.storage.set(keys.thumbprint, thumbprint, o.ttl); + that.storage.set(keys.protocol, utils.getProtocol(), o.ttl); + } + that._mergeProcessedData(processedData); + } + }, + _transformDatum: function(datum) { + var value = utils.isString(datum) ? datum : datum[this.valueKey], tokens = datum.tokens || utils.tokenizeText(value), item = { + value: value, + tokens: tokens + }; + if (utils.isString(datum)) { + item.datum = {}; + item.datum[this.valueKey] = datum; + } else { + item.datum = datum; + } + item.tokens = utils.filter(item.tokens, function(token) { + return !utils.isBlankString(token); + }); + item.tokens = utils.map(item.tokens, function(token) { + return token.toLowerCase(); + }); + return item; + }, + _processData: function(data) { + var that = this, itemHash = {}, adjacencyList = {}; + utils.each(data, function(i, datum) { + var item = that._transformDatum(datum), id = utils.getUniqueId(item.value); + itemHash[id] = item; + utils.each(item.tokens, function(i, token) { + var character = token.charAt(0), adjacency = adjacencyList[character] || (adjacencyList[character] = [ id ]); + !~utils.indexOf(adjacency, id) && adjacency.push(id); + }); + }); + return { + itemHash: itemHash, + adjacencyList: adjacencyList + }; + }, + _mergeProcessedData: function(processedData) { + var that = this; + utils.mixin(this.itemHash, processedData.itemHash); + utils.each(processedData.adjacencyList, function(character, adjacency) { + var masterAdjacency = that.adjacencyList[character]; + that.adjacencyList[character] = masterAdjacency ? masterAdjacency.concat(adjacency) : adjacency; + }); + }, + _getLocalSuggestions: function(terms) { + var that = this, firstChars = [], lists = [], shortestList, suggestions = []; + utils.each(terms, function(i, term) { + var firstChar = term.charAt(0); + !~utils.indexOf(firstChars, firstChar) && firstChars.push(firstChar); + }); + utils.each(firstChars, function(i, firstChar) { + var list = that.adjacencyList[firstChar]; + if (!list) { + return false; + } + lists.push(list); + if (!shortestList || list.length < shortestList.length) { + shortestList = list; + } + }); + if (lists.length < firstChars.length) { + return []; + } + utils.each(shortestList, function(i, id) { + var item = that.itemHash[id], isCandidate, isMatch; + isCandidate = utils.every(lists, function(list) { + return ~utils.indexOf(list, id); + }); + isMatch = isCandidate && utils.every(terms, function(term) { + return utils.some(item.tokens, function(token) { + return token.indexOf(term) === 0; + }); + }); + isMatch && suggestions.push(item); + }); + return suggestions; + }, + initialize: function() { + var deferred; + this.local && this._processLocalData(this.local); + this.transport = this.remote ? new Transport(this.remote) : null; + deferred = this.prefetch ? this._loadPrefetchData(this.prefetch) : $.Deferred().resolve(); + this.local = this.prefetch = this.remote = null; + this.initialize = function() { + return deferred; + }; + return deferred; + }, + getSuggestions: function(query, cb) { + var that = this, terms, suggestions, cacheHit = false; + if (query.length < this.minLength) { + return; + } + terms = utils.tokenizeQuery(query); + suggestions = this._getLocalSuggestions(terms).slice(0, this.limit); + if (suggestions.length < this.limit && this.transport) { + cacheHit = this.transport.get(query, processRemoteData); + } + !cacheHit && cb && cb(suggestions); + function processRemoteData(data) { + suggestions = suggestions.slice(0); + utils.each(data, function(i, datum) { + var item = that._transformDatum(datum), isDuplicate; + isDuplicate = utils.some(suggestions, function(suggestion) { + //return item.value === suggestion.value; + return false; + }); + !isDuplicate && suggestions.push(item); + return suggestions.length < that.limit; + }); + cb && cb(suggestions); + } + } + }); + return Dataset; + function compileTemplate(template, engine, valueKey) { + var renderFn, compiledTemplate; + if (utils.isFunction(template)) { + renderFn = template; + } else if (utils.isString(template)) { + compiledTemplate = engine.compile(template); + renderFn = utils.bind(compiledTemplate.render, compiledTemplate); + } else { + renderFn = function(context) { + return "

    " + context[valueKey] + "

    "; + }; + } + return renderFn; + } + }(); + var InputView = function() { + function InputView(o) { + var that = this; + utils.bindAll(this); + this.specialKeyCodeMap = { + // START METAMAPS CODE + //9: "tab", + // END METAMAPS CODE + 27: "esc", + 37: "left", + 39: "right", + 13: "enter", + 38: "up", + 40: "down" + }; + this.$hint = $(o.hint); + this.$input = $(o.input).on("blur.tt", this._handleBlur).on("focus.tt", this._handleFocus).on("keydown.tt", this._handleSpecialKeyEvent); + if (!utils.isMsie()) { + this.$input.on("input.tt", this._compareQueryToInputValue); + } else { + this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { + if (that.specialKeyCodeMap[$e.which || $e.keyCode]) { + return; + } + utils.defer(that._compareQueryToInputValue); + }); + } + this.query = this.$input.val(); + this.$overflowHelper = buildOverflowHelper(this.$input); + } + utils.mixin(InputView.prototype, EventTarget, { + _handleFocus: function() { this.trigger("focused"); }, - _onKeydown: function onKeydown($e) { - var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; - this._managePreventDefault(keyName, $e); - if (keyName && this._shouldTrigger(keyName, $e)) { - this.trigger(keyName + "Keyed", $e); - } + _handleBlur: function() { + this.trigger("blured"); }, - _onInput: function onInput() { - this._setQuery(this.getInputValue()); - this.clearHintIfInvalid(); - this._checkLanguageDirection(); + _handleSpecialKeyEvent: function($e) { + var keyName = this.specialKeyCodeMap[$e.which || $e.keyCode]; + keyName && this.trigger(keyName + "Keyed", $e); }, - _managePreventDefault: function managePreventDefault(keyName, $e) { - var preventDefault; - switch (keyName) { - case "up": - case "down": - preventDefault = !withModifier($e); - break; - - default: - preventDefault = false; - } - preventDefault && $e.preventDefault(); - }, - _shouldTrigger: function shouldTrigger(keyName, $e) { - var trigger; - switch (keyName) { - case "tab": - trigger = !withModifier($e); - break; - - default: - trigger = true; - } - return trigger; - }, - _checkLanguageDirection: function checkLanguageDirection() { - var dir = (this.$input.css("direction") || "ltr").toLowerCase(); - if (this.dir !== dir) { - this.dir = dir; - this.$hint.attr("dir", dir); - this.trigger("langDirChanged", dir); - } - }, - _setQuery: function setQuery(val, silent) { - var areEquivalent, hasDifferentWhitespace; - areEquivalent = areQueriesEquivalent(val, this.query); - hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false; - this.query = val; - if (!silent && !areEquivalent) { - this.trigger("queryChanged", this.query); - } else if (!silent && hasDifferentWhitespace) { - this.trigger("whitespaceChanged", this.query); - } - }, - bind: function() { - var that = this, onBlur, onFocus, onKeydown, onInput; - onBlur = _.bind(this._onBlur, this); - onFocus = _.bind(this._onFocus, this); - onKeydown = _.bind(this._onKeydown, this); - onInput = _.bind(this._onInput, this); - this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); - if (!_.isMsie() || _.isMsie() > 9) { - this.$input.on("input.tt", onInput); - } else { - this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { - if (specialKeyCodeMap[$e.which || $e.keyCode]) { - return; - } - _.defer(_.bind(that._onInput, that, $e)); + _compareQueryToInputValue: function() { + var inputValue = this.getInputValue(), isSameQuery = compareQueries(this.query, inputValue), isSameQueryExceptWhitespace = isSameQuery ? this.query.length !== inputValue.length : false; + if (isSameQueryExceptWhitespace) { + this.trigger("whitespaceChanged", { + value: this.query + }); + } else if (!isSameQuery) { + this.trigger("queryChanged", { + value: this.query = inputValue }); } - return this; }, - focus: function focus() { + destroy: function() { + this.$hint.off(".tt"); + this.$input.off(".tt"); + this.$hint = this.$input = this.$overflowHelper = null; + }, + focus: function() { this.$input.focus(); }, - blur: function blur() { + blur: function() { this.$input.blur(); }, - getLangDir: function getLangDir() { - return this.dir; + getQuery: function() { + return this.query; }, - getQuery: function getQuery() { - return this.query || ""; + setQuery: function(query) { + this.query = query; }, - setQuery: function setQuery(val, silent) { - this.setInputValue(val); - this._setQuery(val, silent); - }, - hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() { - return this.query !== this.queryWhenFocused; - }, - getInputValue: function getInputValue() { + getInputValue: function() { return this.$input.val(); }, - setInputValue: function setInputValue(value) { + setInputValue: function(value, silent) { this.$input.val(value); - this.clearHintIfInvalid(); - this._checkLanguageDirection(); + !silent && this._compareQueryToInputValue(); }, - resetInputValue: function resetInputValue() { - this.setInputValue(this.query); - }, - getHint: function getHint() { + getHintValue: function() { return this.$hint.val(); }, - setHint: function setHint(value) { + setHintValue: function(value) { this.$hint.val(value); }, - clearHint: function clearHint() { - this.setHint(""); + getLanguageDirection: function() { + return (this.$input.css("direction") || "ltr").toLowerCase(); }, - clearHintIfInvalid: function clearHintIfInvalid() { - var val, hint, valIsPrefixOfHint, isValid; - val = this.getInputValue(); - hint = this.getHint(); - valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; - isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow(); - !isValid && this.clearHint(); - }, - hasFocus: function hasFocus() { - return this.$input.is(":focus"); - }, - hasOverflow: function hasOverflow() { - var constraint = this.$input.width() - 2; + isOverflow: function() { this.$overflowHelper.text(this.getInputValue()); - return this.$overflowHelper.width() >= constraint; + return this.$overflowHelper.width() > this.$input.width(); }, isCursorAtEnd: function() { - var valueLength, selectionStart, range; - valueLength = this.$input.val().length; - selectionStart = this.$input[0].selectionStart; - if (_.isNumber(selectionStart)) { + var valueLength = this.$input.val().length, selectionStart = this.$input[0].selectionStart, range; + if (utils.isNumber(selectionStart)) { return selectionStart === valueLength; } else if (document.selection) { range = document.selection.createRange(); @@ -610,20 +661,15 @@ return valueLength === range.text.length; } return true; - }, - destroy: function destroy() { - this.$hint.off(".tt"); - this.$input.off(".tt"); - this.$overflowHelper.remove(); - this.$hint = this.$input = this.$overflowHelper = $("
    "); } }); - return Input; + return InputView; function buildOverflowHelper($input) { - return $('').css({ + return $("").css({ position: "absolute", + left: "-9999px", visibility: "hidden", - whiteSpace: "pre", + whiteSpace: "nowrap", fontFamily: $input.css("font-family"), fontSize: $input.css("font-size"), fontStyle: $input.css("font-style"), @@ -636,903 +682,484 @@ textTransform: $input.css("text-transform") }).insertAfter($input); } - function areQueriesEquivalent(a, b) { - return Input.normalizeQuery(a) === Input.normalizeQuery(b); - } - function withModifier($e) { - return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; + function compareQueries(a, b) { + a = (a || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); + b = (b || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); + return a === b; } }(); - var Dataset = function() { - "use strict"; - var keys, nameGenerator; - keys = { - val: "tt-selectable-display", - obj: "tt-selectable-object" + var DropdownView = function() { + var html = { + suggestionsList: '' + }, css = { + suggestionsList: { + display: "block" + }, + suggestion: { + whiteSpace: "nowrap", + cursor: "pointer" + }, + suggestionChild: { + whiteSpace: "normal" + } }; - nameGenerator = _.getIdGenerator(); - function Dataset(o, www) { - o = o || {}; - o.templates = o.templates || {}; - o.templates.notFound = o.templates.notFound || o.templates.empty; - if (!o.source) { - $.error("missing source"); - } - if (!o.node) { - $.error("missing node"); - } - if (o.name && !isValidName(o.name)) { - $.error("invalid dataset name: " + o.name); - } - www.mixin(this); - this.highlight = !!o.highlight; - this.name = o.name || nameGenerator(); - this.limit = o.limit || 5; - this.displayFn = getDisplayFn(o.display || o.displayKey); - this.templates = getTemplates(o.templates, this.displayFn); - this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source; - this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async; - this._resetLastSuggestion(); - this.$el = $(o.node).addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name); + function DropdownView(o) { + utils.bindAll(this); + this.isOpen = false; + this.isEmpty = true; + this.isMouseOverDropdown = false; + this.$menu = $(o.menu).on("mouseenter.tt", this._handleMouseenter).on("mouseleave.tt", this._handleMouseleave).on("click.tt", ".tt-suggestion", this._handleSelection).on("mouseover.tt", ".tt-suggestion", this._handleMouseover); } - Dataset.extractData = function extractData(el) { - var $el = $(el); - if ($el.data(keys.obj)) { - return { - val: $el.data(keys.val) || "", - obj: $el.data(keys.obj) || null + utils.mixin(DropdownView.prototype, EventTarget, { + _handleMouseenter: function() { + this.isMouseOverDropdown = true; + }, + _handleMouseleave: function() { + this.isMouseOverDropdown = false; + + // START METAMAPS CODE + this._getSuggestions().removeClass("tt-is-under-cursor"); + this._getSuggestions().removeClass("tt-is-under-mouse-cursor"); + // END METAMAPS CODE + }, + _handleMouseover: function($e) { + var $suggestion = $($e.currentTarget); + this._getSuggestions().removeClass("tt-is-under-cursor"); + // START METAMAPS CODE + this._getSuggestions().removeClass("tt-is-under-mouse-cursor"); + $suggestion.addClass("tt-is-under-mouse-cursor"); + // ORIGINAL CODE $suggestion.addClass("tt-is-under-cursor"); + }, + _handleSelection: function($e) { + var $suggestion = $($e.currentTarget); + this.trigger("suggestionSelected", extractSuggestion($suggestion)); + }, + _show: function() { + this.$menu.css("display", "block"); + }, + _hide: function() { + this.$menu.hide(); + }, + _moveCursor: function(increment) { + var $suggestions, $cur, nextIndex, $underCursor; + if (!this.isVisible()) { + return; + } + $suggestions = this._getSuggestions(); + $cur = $suggestions.filter(".tt-is-under-cursor"); + $cur.removeClass("tt-is-under-cursor"); + nextIndex = $suggestions.index($cur) + increment; + nextIndex = (nextIndex + 1) % ($suggestions.length + 1) - 1; + if (nextIndex === -1) { + this.trigger("cursorRemoved"); + return; + } else if (nextIndex < -1) { + nextIndex = $suggestions.length - 1; + } + $underCursor = $suggestions.eq(nextIndex).addClass("tt-is-under-cursor"); + this._ensureVisibility($underCursor); + this.trigger("cursorMoved", extractSuggestion($underCursor)); + }, + _getSuggestions: function() { + return this.$menu.find(".tt-suggestions > .tt-suggestion"); + }, + _ensureVisibility: function($el) { + var menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10), menuScrollTop = this.$menu.scrollTop(), elTop = $el.position().top, elBottom = elTop + $el.outerHeight(true); + if (elTop < 0) { + this.$menu.scrollTop(menuScrollTop + elTop); + } else if (menuHeight < elBottom) { + this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight)); + } + }, + destroy: function() { + this.$menu.off(".tt"); + this.$menu = null; + }, + isVisible: function() { + return this.isOpen && !this.isEmpty; + }, + closeUnlessMouseIsOverDropdown: function() { + if (!this.isMouseOverDropdown) { + this.close(); + } + }, + close: function() { + if (this.isOpen) { + this.isOpen = false; + this.isMouseOverDropdown = false; + this._hide(); + this.$menu.find(".tt-suggestions > .tt-suggestion").removeClass("tt-is-under-cursor"); + this.trigger("closed"); + } + }, + open: function() { + if (!this.isOpen) { + this.isOpen = true; + !this.isEmpty && this._show(); + this.trigger("opened"); + } + }, + setLanguageDirection: function(dir) { + var ltrCss = { + left: "0", + right: "auto" + }, rtlCss = { + left: "auto", + right: " 0" }; - } - return null; - }; - _.mixin(Dataset.prototype, EventEmitter, { - _overwrite: function overwrite(query, suggestions) { - suggestions = suggestions || []; - if (suggestions.length) { - this._renderSuggestions(query, suggestions); - } else if (this.async && this.templates.pending) { - this._renderPending(query); - } else if (!this.async && this.templates.notFound) { - this._renderNotFound(query); + dir === "ltr" ? this.$menu.css(ltrCss) : this.$menu.css(rtlCss); + }, + moveCursorUp: function() { + this._moveCursor(-1); + }, + moveCursorDown: function() { + this._moveCursor(+1); + }, + getSuggestionUnderCursor: function() { + var $suggestion = this._getSuggestions().filter(".tt-is-under-cursor").first(); + return $suggestion.length > 0 ? extractSuggestion($suggestion) : null; + }, + getFirstSuggestion: function() { + var $suggestion = this._getSuggestions().first(); + return $suggestion.length > 0 ? extractSuggestion($suggestion) : null; + }, + renderSuggestions: function(dataset, suggestions) { + var datasetClassName = "tt-dataset-" + dataset.name, wrapper = '
    %body
    ', compiledHtml, $suggestionsList, $dataset = this.$menu.find("." + datasetClassName), elBuilder, fragment, $el; + if ($dataset.length === 0) { + $suggestionsList = $(html.suggestionsList).css(css.suggestionsList); + $dataset = $("
    ").addClass(datasetClassName).append(dataset.header).append($suggestionsList).append(dataset.footer).appendTo(this.$menu); + } + if (suggestions.length > 0) { + this.isEmpty = false; + this.isOpen && this._show(); + elBuilder = document.createElement("div"); + fragment = document.createDocumentFragment(); + utils.each(suggestions, function(i, suggestion) { + suggestion.dataset = dataset.name; + compiledHtml = dataset.template(suggestion.datum); + elBuilder.innerHTML = wrapper.replace("%body", compiledHtml); + $el = $(elBuilder.firstChild).css(css.suggestion).data("suggestion", suggestion); + $el.children().each(function() { + $(this).css(css.suggestionChild); + }); + fragment.appendChild($el[0]); + }); + $dataset.show().find(".tt-suggestions").html(fragment); } else { - this._empty(); + this.clearSuggestions(dataset.name); } - this.trigger("rendered", this.name, suggestions, false); + this.trigger("suggestionsRendered"); }, - _append: function append(query, suggestions) { - suggestions = suggestions || []; - if (suggestions.length && this.$lastSuggestion.length) { - this._appendSuggestions(query, suggestions); - } else if (suggestions.length) { - this._renderSuggestions(query, suggestions); - } else if (!this.$lastSuggestion.length && this.templates.notFound) { - this._renderNotFound(query); + clearSuggestions: function(datasetName) { + var $datasets = datasetName ? this.$menu.find(".tt-dataset-" + datasetName) : this.$menu.find('[class^="tt-dataset-"]'), $suggestions = $datasets.find(".tt-suggestions"); + $datasets.hide(); + $suggestions.empty(); + if (this._getSuggestions().length === 0) { + this.isEmpty = true; + this._hide(); } - this.trigger("rendered", this.name, suggestions, true); + } + }); + return DropdownView; + function extractSuggestion($el) { + return $el.data("suggestion"); + } + }(); + var TypeaheadView = function() { + var html = { + wrapper: '', + hint: '', + dropdown: '' + }, css = { + wrapper: { + position: "relative", + display: "inline-block" }, - _renderSuggestions: function renderSuggestions(query, suggestions) { - var $fragment; - $fragment = this._getSuggestionsFragment(query, suggestions); - this.$lastSuggestion = $fragment.children().last(); - this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions)); + hint: { + position: "absolute", + top: "0", + left: "0", + borderColor: "transparent", + boxShadow: "none" }, - _appendSuggestions: function appendSuggestions(query, suggestions) { - var $fragment, $lastSuggestion; - $fragment = this._getSuggestionsFragment(query, suggestions); - $lastSuggestion = $fragment.children().last(); - this.$lastSuggestion.after($fragment); - this.$lastSuggestion = $lastSuggestion; + query: { + position: "relative", + verticalAlign: "top", + backgroundColor: "transparent" }, - _renderPending: function renderPending(query) { - var template = this.templates.pending; - this._resetLastSuggestion(); - template && this.$el.html(template({ - query: query, - dataset: this.name - })); + dropdown: { + position: "absolute", + top: "100%", + left: "0", + zIndex: "100", + display: "none" + } + }; + if (utils.isMsie()) { + utils.mixin(css.query, { + backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" + }); + } + if (utils.isMsie() && utils.isMsie() <= 7) { + utils.mixin(css.wrapper, { + display: "inline", + zoom: "1" + }); + utils.mixin(css.query, { + marginTop: "-1px" + }); + } + function TypeaheadView(o) { + var $menu, $input, $hint; + utils.bindAll(this); + this.$node = buildDomStructure(o.input); + this.datasets = o.datasets; + this.dir = null; + this.eventBus = o.eventBus; + $menu = this.$node.find(".tt-dropdown-menu"); + $input = this.$node.find(".tt-query"); + $hint = this.$node.find(".tt-hint"); + this.dropdownView = new DropdownView({ + menu: $menu + }).on("suggestionSelected", this._handleSelection).on("cursorMoved", this._clearHint).on("cursorMoved", this._setInputValueToSuggestionUnderCursor).on("cursorRemoved", this._setInputValueToQuery).on("cursorRemoved", this._updateHint).on("suggestionsRendered", this._updateHint).on("opened", this._updateHint).on("closed", this._clearHint).on("opened closed", this._propagateEvent); + // START METAMAPS CODE + this.dropdownView.on('suggestionsRendered', this._suggestionsRendered); + // END METAMAPS CODE + + this.inputView = new InputView({ + input: $input, + hint: $hint + }).on("focused", this._openDropdown).on("blured", this._closeDropdown).on("blured", this._setInputValueToQuery).on("enterKeyed tabKeyed", this._handleSelection).on("queryChanged", this._clearHint).on("queryChanged", this._clearSuggestions).on("queryChanged", this._getSuggestions).on("whitespaceChanged", this._updateHint).on("queryChanged whitespaceChanged", this._openDropdown).on("queryChanged whitespaceChanged", this._setLanguageDirection).on("escKeyed", this._closeDropdown).on("escKeyed", this._setInputValueToQuery).on("tabKeyed upKeyed downKeyed", this._managePreventDefault).on("upKeyed downKeyed", this._moveDropdownCursor).on("upKeyed downKeyed", this._openDropdown).on("tabKeyed leftKeyed rightKeyed", this._autocomplete); + // START METAMAPS CODE + this.inputView.on('queryChanged', this._queryChanged); + // END METAMAPS CODE + } + utils.mixin(TypeaheadView.prototype, EventTarget, { + _managePreventDefault: function(e) { + var $e = e.data, hint, inputValue, preventDefault = false; + switch (e.type) { + case "tabKeyed": + hint = this.inputView.getHintValue(); + inputValue = this.inputView.getInputValue(); + preventDefault = hint && hint !== inputValue; + break; + + case "upKeyed": + case "downKeyed": + preventDefault = !$e.shiftKey && !$e.ctrlKey && !$e.metaKey; + break; + } + preventDefault && $e.preventDefault(); }, - _renderNotFound: function renderNotFound(query) { - var template = this.templates.notFound; - this._resetLastSuggestion(); - template && this.$el.html(template({ - query: query, - dataset: this.name - })); + _setLanguageDirection: function() { + var dir = this.inputView.getLanguageDirection(); + if (dir !== this.dir) { + this.dir = dir; + this.$node.css("direction", dir); + this.dropdownView.setLanguageDirection(dir); + } }, - _empty: function empty() { - this.$el.empty(); - this._resetLastSuggestion(); + // START METAMAPS CODE + _suggestionsRendered: function() { + this.eventBus.trigger('suggestionsRendered'); }, - _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) { - var that = this, fragment; - fragment = document.createDocumentFragment(); - _.each(suggestions, function getSuggestionNode(suggestion) { - var $el, context; - context = that._injectQuery(query, suggestion); - $el = $(that.templates.suggestion(context)).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable); - fragment.appendChild($el[0]); + _queryChanged: function() { + this.eventBus.trigger('queryChanged'); + }, + // END METAMAPS CODE + _updateHint: function() { + var suggestion = this.dropdownView.getFirstSuggestion(), hint = suggestion ? suggestion.value : null, dropdownIsVisible = this.dropdownView.isVisible(), inputHasOverflow = this.inputView.isOverflow(), inputValue, query, escapedQuery, beginsWithQuery, match; + if (hint && dropdownIsVisible && !inputHasOverflow) { + inputValue = this.inputView.getInputValue(); + query = inputValue.replace(/\s{2,}/g, " ").replace(/^\s+/g, ""); + escapedQuery = utils.escapeRegExChars(query); + beginsWithQuery = new RegExp("^(?:" + escapedQuery + ")(.*$)", "i"); + match = beginsWithQuery.exec(hint); + this.inputView.setHintValue(inputValue + (match ? match[1] : "")); + } + }, + _clearHint: function() { + this.inputView.setHintValue(""); + }, + _clearSuggestions: function() { + this.dropdownView.clearSuggestions(); + }, + _setInputValueToQuery: function() { + this.inputView.setInputValue(this.inputView.getQuery()); + }, + _setInputValueToSuggestionUnderCursor: function(e) { + var suggestion = e.data; + this.inputView.setInputValue(suggestion.value, true); + }, + _openDropdown: function() { + this.dropdownView.open(); + }, + _closeDropdown: function(e) { + this.dropdownView[e.type === "blured" ? "closeUnlessMouseIsOverDropdown" : "close"](); + }, + _moveDropdownCursor: function(e) { + var $e = e.data; + if (!$e.shiftKey && !$e.ctrlKey && !$e.metaKey) { + this.dropdownView[e.type === "upKeyed" ? "moveCursorUp" : "moveCursorDown"](); + } + }, + _handleSelection: function(e) { + var byClick = e.type === "suggestionSelected", suggestion = byClick ? e.data : this.dropdownView.getSuggestionUnderCursor(); + if (suggestion) { + this.inputView.setInputValue(suggestion.value); + byClick ? this.inputView.focus() : e.data.preventDefault(); + byClick && utils.isMsie() ? utils.defer(this.dropdownView.close) : this.dropdownView.close(); + this.eventBus.trigger("selected", suggestion.datum, suggestion.dataset); + } + }, + _getSuggestions: function() { + var that = this, query = this.inputView.getQuery(); + if (utils.isBlankString(query)) { + return; + } + utils.each(this.datasets, function(i, dataset) { + dataset.getSuggestions(query, function(suggestions) { + if (query === that.inputView.getQuery()) { + that.dropdownView.renderSuggestions(dataset, suggestions); + } + }); }); - this.highlight && highlight({ - className: this.classes.highlight, - node: fragment, - pattern: query - }); - return $(fragment); }, - _getFooter: function getFooter(query, suggestions) { - return this.templates.footer ? this.templates.footer({ - query: query, - suggestions: suggestions, - dataset: this.name - }) : null; - }, - _getHeader: function getHeader(query, suggestions) { - return this.templates.header ? this.templates.header({ - query: query, - suggestions: suggestions, - dataset: this.name - }) : null; - }, - _resetLastSuggestion: function resetLastSuggestion() { - this.$lastSuggestion = $(); - }, - _injectQuery: function injectQuery(query, obj) { - return _.isObject(obj) ? _.mixin({ - _query: query - }, obj) : obj; - }, - update: function update(query) { - var that = this, canceled = false, syncCalled = false, rendered = 0; - this.cancel(); - this.cancel = function cancel() { - canceled = true; - that.cancel = $.noop; - that.async && that.trigger("asyncCanceled", query); - }; - this.source(query, sync, async); - !syncCalled && sync([]); - function sync(suggestions) { - if (syncCalled) { + _autocomplete: function(e) { + var isCursorAtEnd, ignoreEvent, query, hint, suggestion; + if (e.type === "rightKeyed" || e.type === "leftKeyed") { + isCursorAtEnd = this.inputView.isCursorAtEnd(); + ignoreEvent = this.inputView.getLanguageDirection() === "ltr" ? e.type === "leftKeyed" : e.type === "rightKeyed"; + if (!isCursorAtEnd || ignoreEvent) { return; } - syncCalled = true; - suggestions = (suggestions || []).slice(0, that.limit); - rendered = suggestions.length; - that._overwrite(query, suggestions); - if (rendered < that.limit && that.async) { - that.trigger("asyncRequested", query); - } } - function async(suggestions) { - suggestions = suggestions || []; - if (!canceled && rendered < that.limit) { - that.cancel = $.noop; - rendered += suggestions.length; - that._append(query, suggestions.slice(0, that.limit - rendered)); - that.async && that.trigger("asyncReceived", query); - } + query = this.inputView.getQuery(); + hint = this.inputView.getHintValue(); + if (hint !== "" && query !== hint) { + suggestion = this.dropdownView.getFirstSuggestion(); + this.inputView.setInputValue(suggestion.value); + this.eventBus.trigger("autocompleted", suggestion.datum, suggestion.dataset); } }, - cancel: $.noop, - clear: function clear() { - this._empty(); - this.cancel(); - this.trigger("cleared"); + _propagateEvent: function(e) { + this.eventBus.trigger(e.type); }, - isEmpty: function isEmpty() { - return this.$el.is(":empty"); + destroy: function() { + this.inputView.destroy(); + this.dropdownView.destroy(); + destroyDomStructure(this.$node); + this.$node = null; }, - destroy: function destroy() { - this.$el = $("
    "); + setQuery: function(query) { + this.inputView.setQuery(query); + this.inputView.setInputValue(query); + this._clearHint(); + this._clearSuggestions(); + this._getSuggestions(); } }); - return Dataset; - function getDisplayFn(display) { - display = display || _.stringify; - return _.isFunction(display) ? display : displayFn; - function displayFn(obj) { - return obj[display]; - } + return TypeaheadView; + function buildDomStructure(input) { + var $wrapper = $(html.wrapper), $dropdown = $(html.dropdown), $input = $(input), $hint = $(html.hint); + $wrapper = $wrapper.css(css.wrapper); + $dropdown = $dropdown.css(css.dropdown); + $hint.css(css.hint).css({ + backgroundAttachment: $input.css("background-attachment"), + backgroundClip: $input.css("background-clip"), + backgroundColor: $input.css("background-color"), + backgroundImage: $input.css("background-image"), + backgroundOrigin: $input.css("background-origin"), + backgroundPosition: $input.css("background-position"), + backgroundRepeat: $input.css("background-repeat"), + backgroundSize: $input.css("background-size") + }); + $input.data("ttAttrs", { + dir: $input.attr("dir"), + autocomplete: $input.attr("autocomplete"), + spellcheck: $input.attr("spellcheck"), + style: $input.attr("style") + }); + $input.addClass("tt-query").attr({ + autocomplete: "off", + spellcheck: false + }).css(css.query); + try { + !$input.attr("dir") && $input.attr("dir", "auto"); + } catch (e) {} + return $input.wrap($wrapper).parent().prepend($hint).append($dropdown); } - function getTemplates(templates, displayFn) { - return { - notFound: templates.notFound && _.templatify(templates.notFound), - pending: templates.pending && _.templatify(templates.pending), - header: templates.header && _.templatify(templates.header), - footer: templates.footer && _.templatify(templates.footer), - suggestion: templates.suggestion || suggestionTemplate - }; - function suggestionTemplate(context) { - return $("
    ").text(displayFn(context)); - } - } - function isValidName(str) { - return /^[_a-zA-Z0-9-]+$/.test(str); - } - }(); - var Menu = function() { - "use strict"; - function Menu(o, www) { - var that = this; - o = o || {}; - if (!o.node) { - $.error("node is required"); - } - www.mixin(this); - this.$node = $(o.node); - this.query = null; - this.datasets = _.map(o.datasets, initializeDataset); - function initializeDataset(oDataset) { - var node = that.$node.find(oDataset.node).first(); - oDataset.node = node.length ? node : $("
    ").appendTo(that.$node); - return new Dataset(oDataset, www); - } - } - _.mixin(Menu.prototype, EventEmitter, { - _onSelectableClick: function onSelectableClick($e) { - this.trigger("selectableClicked", $($e.currentTarget)); - }, - _onRendered: function onRendered(type, dataset, suggestions, async) { - this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); - this.trigger("datasetRendered", dataset, suggestions, async); - }, - _onCleared: function onCleared() { - this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); - this.trigger("datasetCleared"); - }, - _propagate: function propagate() { - this.trigger.apply(this, arguments); - }, - _allDatasetsEmpty: function allDatasetsEmpty() { - return _.every(this.datasets, isDatasetEmpty); - function isDatasetEmpty(dataset) { - return dataset.isEmpty(); - } - }, - _getSelectables: function getSelectables() { - return this.$node.find(this.selectors.selectable); - }, - _removeCursor: function _removeCursor() { - var $selectable = this.getActiveSelectable(); - $selectable && $selectable.removeClass(this.classes.cursor); - }, - _ensureVisible: function ensureVisible($el) { - var elTop, elBottom, nodeScrollTop, nodeHeight; - elTop = $el.position().top; - elBottom = elTop + $el.outerHeight(true); - nodeScrollTop = this.$node.scrollTop(); - nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10); - if (elTop < 0) { - this.$node.scrollTop(nodeScrollTop + elTop); - } else if (nodeHeight < elBottom) { - this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight)); - } - }, - bind: function() { - var that = this, onSelectableClick; - onSelectableClick = _.bind(this._onSelectableClick, this); - this.$node.on("click.tt", this.selectors.selectable, onSelectableClick); - _.each(this.datasets, function(dataset) { - dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that); - }); - return this; - }, - isOpen: function isOpen() { - return this.$node.hasClass(this.classes.open); - }, - open: function open() { - this.$node.addClass(this.classes.open); - }, - close: function close() { - this.$node.removeClass(this.classes.open); - this._removeCursor(); - }, - setLanguageDirection: function setLanguageDirection(dir) { - this.$node.attr("dir", dir); - }, - selectableRelativeToCursor: function selectableRelativeToCursor(delta) { - var $selectables, $oldCursor, oldIndex, newIndex; - $oldCursor = this.getActiveSelectable(); - $selectables = this._getSelectables(); - oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1; - newIndex = oldIndex + delta; - newIndex = (newIndex + 1) % ($selectables.length + 1) - 1; - newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex; - return newIndex === -1 ? null : $selectables.eq(newIndex); - }, - setCursor: function setCursor($selectable) { - this._removeCursor(); - if ($selectable = $selectable && $selectable.first()) { - $selectable.addClass(this.classes.cursor); - this._ensureVisible($selectable); - } - }, - getSelectableData: function getSelectableData($el) { - return $el && $el.length ? Dataset.extractData($el) : null; - }, - getActiveSelectable: function getActiveSelectable() { - var $selectable = this._getSelectables().filter(this.selectors.cursor).first(); - return $selectable.length ? $selectable : null; - }, - getTopSelectable: function getTopSelectable() { - var $selectable = this._getSelectables().first(); - return $selectable.length ? $selectable : null; - }, - update: function update(query) { - var isValidUpdate = query !== this.query; - if (isValidUpdate) { - this.query = query; - _.each(this.datasets, updateDataset); - } - return isValidUpdate; - function updateDataset(dataset) { - dataset.update(query); - } - }, - empty: function empty() { - _.each(this.datasets, clearDataset); - this.query = null; - this.$node.addClass(this.classes.empty); - function clearDataset(dataset) { - dataset.clear(); - } - }, - destroy: function destroy() { - this.$node.off(".tt"); - this.$node = $("
    "); - _.each(this.datasets, destroyDataset); - function destroyDataset(dataset) { - dataset.destroy(); - } - } - }); - return Menu; - }(); - var DefaultMenu = function() { - "use strict"; - var s = Menu.prototype; - function DefaultMenu() { - Menu.apply(this, [].slice.call(arguments, 0)); - } - _.mixin(DefaultMenu.prototype, Menu.prototype, { - open: function open() { - !this._allDatasetsEmpty() && this._show(); - return s.open.apply(this, [].slice.call(arguments, 0)); - }, - close: function close() { - this._hide(); - return s.close.apply(this, [].slice.call(arguments, 0)); - }, - _onRendered: function onRendered() { - if (this._allDatasetsEmpty()) { - this._hide(); - } else { - this.isOpen() && this._show(); - } - return s._onRendered.apply(this, [].slice.call(arguments, 0)); - }, - _onCleared: function onCleared() { - if (this._allDatasetsEmpty()) { - this._hide(); - } else { - this.isOpen() && this._show(); - } - return s._onCleared.apply(this, [].slice.call(arguments, 0)); - }, - setLanguageDirection: function setLanguageDirection(dir) { - this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl); - return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0)); - }, - _hide: function hide() { - this.$node.hide(); - }, - _show: function show() { - this.$node.css("display", "block"); - } - }); - return DefaultMenu; - }(); - var Typeahead = function() { - "use strict"; - function Typeahead(o, www) { - var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged; - o = o || {}; - if (!o.input) { - $.error("missing input"); - } - if (!o.menu) { - $.error("missing menu"); - } - if (!o.eventBus) { - $.error("missing event bus"); - } - www.mixin(this); - this.eventBus = o.eventBus; - this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; - this.input = o.input; - this.menu = o.menu; - this.enabled = true; - this.active = false; - this.input.hasFocus() && this.activate(); - this.dir = this.input.getLangDir(); - this._hacks(); - this.menu.bind().onSync("selectableClicked", this._onSelectableClicked, this).onSync("asyncRequested", this._onAsyncRequested, this).onSync("asyncCanceled", this._onAsyncCanceled, this).onSync("asyncReceived", this._onAsyncReceived, this).onSync("datasetRendered", this._onDatasetRendered, this).onSync("datasetCleared", this._onDatasetCleared, this); - onFocused = c(this, "activate", "open", "_onFocused"); - onBlurred = c(this, "deactivate", "_onBlurred"); - onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed"); - onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed"); - onEscKeyed = c(this, "isActive", "_onEscKeyed"); - onUpKeyed = c(this, "isActive", "open", "_onUpKeyed"); - onDownKeyed = c(this, "isActive", "open", "_onDownKeyed"); - onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed"); - onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed"); - onQueryChanged = c(this, "_openIfActive", "_onQueryChanged"); - onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged"); - this.input.bind().onSync("focused", onFocused, this).onSync("blurred", onBlurred, this).onSync("enterKeyed", onEnterKeyed, this).onSync("tabKeyed", onTabKeyed, this).onSync("escKeyed", onEscKeyed, this).onSync("upKeyed", onUpKeyed, this).onSync("downKeyed", onDownKeyed, this).onSync("leftKeyed", onLeftKeyed, this).onSync("rightKeyed", onRightKeyed, this).onSync("queryChanged", onQueryChanged, this).onSync("whitespaceChanged", onWhitespaceChanged, this).onSync("langDirChanged", this._onLangDirChanged, this); - } - _.mixin(Typeahead.prototype, { - _hacks: function hacks() { - var $input, $menu; - $input = this.input.$input || $("
    "); - $menu = this.menu.$node || $("
    "); - $input.on("blur.tt", function($e) { - var active, isActive, hasActive; - active = document.activeElement; - isActive = $menu.is(active); - hasActive = $menu.has(active).length > 0; - if (_.isMsie() && (isActive || hasActive)) { - $e.preventDefault(); - $e.stopImmediatePropagation(); - _.defer(function() { - $input.focus(); - }); - } - }); - $menu.on("mousedown.tt", function($e) { - $e.preventDefault(); - }); - }, - _onSelectableClicked: function onSelectableClicked(type, $el) { - this.select($el); - }, - _onDatasetCleared: function onDatasetCleared() { - this._updateHint(); - }, - _onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) { - this._updateHint(); - this.eventBus.trigger("render", suggestions, async, dataset); - }, - _onAsyncRequested: function onAsyncRequested(type, dataset, query) { - this.eventBus.trigger("asyncrequest", query, dataset); - }, - _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) { - this.eventBus.trigger("asynccancel", query, dataset); - }, - _onAsyncReceived: function onAsyncReceived(type, dataset, query) { - this.eventBus.trigger("asyncreceive", query, dataset); - }, - _onFocused: function onFocused() { - this._minLengthMet() && this.menu.update(this.input.getQuery()); - }, - _onBlurred: function onBlurred() { - if (this.input.hasQueryChangedSinceLastFocus()) { - this.eventBus.trigger("change", this.input.getQuery()); - } - }, - _onEnterKeyed: function onEnterKeyed(type, $e) { - var $selectable; - if ($selectable = this.menu.getActiveSelectable()) { - this.select($selectable) && $e.preventDefault(); - } - }, - _onTabKeyed: function onTabKeyed(type, $e) { - var $selectable; - if ($selectable = this.menu.getActiveSelectable()) { - this.select($selectable) && $e.preventDefault(); - } else if ($selectable = this.menu.getTopSelectable()) { - this.autocomplete($selectable) && $e.preventDefault(); - } - }, - _onEscKeyed: function onEscKeyed() { - this.close(); - }, - _onUpKeyed: function onUpKeyed() { - this.moveCursor(-1); - }, - _onDownKeyed: function onDownKeyed() { - this.moveCursor(+1); - }, - _onLeftKeyed: function onLeftKeyed() { - if (this.dir === "rtl" && this.input.isCursorAtEnd()) { - this.autocomplete(this.menu.getTopSelectable()); - } - }, - _onRightKeyed: function onRightKeyed() { - if (this.dir === "ltr" && this.input.isCursorAtEnd()) { - this.autocomplete(this.menu.getTopSelectable()); - } - }, - _onQueryChanged: function onQueryChanged(e, query) { - this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty(); - }, - _onWhitespaceChanged: function onWhitespaceChanged() { - this._updateHint(); - }, - _onLangDirChanged: function onLangDirChanged(e, dir) { - if (this.dir !== dir) { - this.dir = dir; - this.menu.setLanguageDirection(dir); - } - }, - _openIfActive: function openIfActive() { - this.isActive() && this.open(); - }, - _minLengthMet: function minLengthMet(query) { - query = _.isString(query) ? query : this.input.getQuery() || ""; - return query.length >= this.minLength; - }, - _updateHint: function updateHint() { - var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match; - $selectable = this.menu.getTopSelectable(); - data = this.menu.getSelectableData($selectable); - val = this.input.getInputValue(); - if (data && !_.isBlankString(val) && !this.input.hasOverflow()) { - query = Input.normalizeQuery(val); - escapedQuery = _.escapeRegExChars(query); - frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i"); - match = frontMatchRegEx.exec(data.val); - match && this.input.setHint(val + match[1]); - } else { - this.input.clearHint(); - } - }, - isEnabled: function isEnabled() { - return this.enabled; - }, - enable: function enable() { - this.enabled = true; - }, - disable: function disable() { - this.enabled = false; - }, - isActive: function isActive() { - return this.active; - }, - activate: function activate() { - if (this.isActive()) { - return true; - } else if (!this.isEnabled() || this.eventBus.before("active")) { - return false; - } else { - this.active = true; - this.eventBus.trigger("active"); - return true; - } - }, - deactivate: function deactivate() { - if (!this.isActive()) { - return true; - } else if (this.eventBus.before("idle")) { - return false; - } else { - this.active = false; - this.close(); - this.eventBus.trigger("idle"); - return true; - } - }, - isOpen: function isOpen() { - return this.menu.isOpen(); - }, - open: function open() { - if (!this.isOpen() && !this.eventBus.before("open")) { - this.menu.open(); - this._updateHint(); - this.eventBus.trigger("open"); - } - return this.isOpen(); - }, - close: function close() { - if (this.isOpen() && !this.eventBus.before("close")) { - this.menu.close(); - this.input.clearHint(); - this.input.resetInputValue(); - this.eventBus.trigger("close"); - } - return !this.isOpen(); - }, - setVal: function setVal(val) { - this.input.setQuery(_.toStr(val)); - }, - getVal: function getVal() { - return this.input.getQuery(); - }, - select: function select($selectable) { - var data = this.menu.getSelectableData($selectable); - if (data && !this.eventBus.before("select", data.obj)) { - this.input.setQuery(data.val, true); - this.eventBus.trigger("select", data.obj); - this.close(); - return true; - } - return false; - }, - autocomplete: function autocomplete($selectable) { - var query, data, isValid; - query = this.input.getQuery(); - data = this.menu.getSelectableData($selectable); - isValid = data && query !== data.val; - if (isValid && !this.eventBus.before("autocomplete", data.obj)) { - this.input.setQuery(data.val); - this.eventBus.trigger("autocomplete", data.obj); - return true; - } - return false; - }, - moveCursor: function moveCursor(delta) { - var query, $candidate, data, payload, cancelMove; - query = this.input.getQuery(); - $candidate = this.menu.selectableRelativeToCursor(delta); - data = this.menu.getSelectableData($candidate); - payload = data ? data.obj : null; - cancelMove = this._minLengthMet() && this.menu.update(query); - if (!cancelMove && !this.eventBus.before("cursorchange", payload)) { - this.menu.setCursor($candidate); - if (data) { - this.input.setInputValue(data.val); - } else { - this.input.resetInputValue(); - this._updateHint(); - } - this.eventBus.trigger("cursorchange", payload); - return true; - } - return false; - }, - destroy: function destroy() { - this.input.destroy(); - this.menu.destroy(); - } - }); - return Typeahead; - function c(ctx) { - var methods = [].slice.call(arguments, 1); - return function() { - var args = [].slice.call(arguments); - _.each(methods, function(method) { - return ctx[method].apply(ctx, args); - }); - }; + function destroyDomStructure($node) { + var $input = $node.find(".tt-query"); + utils.each($input.data("ttAttrs"), function(key, val) { + utils.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); + }); + $input.detach().removeData("ttAttrs").removeClass("tt-query").insertAfter($node); + $node.remove(); } }(); (function() { - "use strict"; - var old, keys, methods; - old = $.fn.typeahead; - keys = { - www: "tt-www", - attrs: "tt-attrs", - typeahead: "tt-typeahead" - }; + var cache = {}, viewKey = "ttView", methods; methods = { - initialize: function initialize(o, datasets) { - var www; - datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); - o = o || {}; - www = WWW(o.classNames); - return this.each(attach); - function attach() { - var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, typeahead, MenuConstructor; - _.each(datasets, function(d) { - d.highlight = !!o.highlight; - }); - $input = $(this); - $wrapper = $(www.html.wrapper); - $hint = $elOrNull(o.hint); - $menu = $elOrNull(o.menu); - defaultHint = o.hint !== false && !$hint; - defaultMenu = o.menu !== false && !$menu; - defaultHint && ($hint = buildHintFromInput($input, www)); - defaultMenu && ($menu = $(www.html.menu).css(www.css.menu)); - $hint && $hint.val(""); - $input = prepInput($input, www); - if (defaultHint || defaultMenu) { - $wrapper.css(www.css.wrapper); - $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint); - $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null); + initialize: function(datasetDefs) { + var datasets; + datasetDefs = utils.isArray(datasetDefs) ? datasetDefs : [ datasetDefs ]; + if (datasetDefs.length === 0) { + $.error("no datasets provided"); + } + datasets = utils.map(datasetDefs, function(o) { + var dataset = cache[o.name] ? cache[o.name] : new Dataset(o); + if (o.name) { + cache[o.name] = dataset; } - MenuConstructor = defaultMenu ? DefaultMenu : Menu; - eventBus = new EventBus({ + return dataset; + }); + return this.each(initialize); + function initialize() { + var $input = $(this), deferreds, eventBus = new EventBus({ el: $input }); - input = new Input({ - hint: $hint, - input: $input - }, www); - menu = new MenuConstructor({ - node: $menu, + deferreds = utils.map(datasets, function(dataset) { + return dataset.initialize(); + }); + $input.data(viewKey, new TypeaheadView({ + input: $input, + eventBus: eventBus = new EventBus({ + el: $input + }), datasets: datasets - }, www); - typeahead = new Typeahead({ - input: input, - menu: menu, - eventBus: eventBus, - minLength: o.minLength - }, www); - $input.data(keys.www, www); - $input.data(keys.typeahead, typeahead); + })); + $.when.apply($, deferreds).always(function() { + utils.defer(function() { + eventBus.trigger("initialized"); + }); + }); } }, - isEnabled: function isEnabled() { - var enabled; - ttEach(this.first(), function(t) { - enabled = t.isEnabled(); - }); - return enabled; - }, - enable: function enable() { - ttEach(this, function(t) { - t.enable(); - }); - return this; - }, - disable: function disable() { - ttEach(this, function(t) { - t.disable(); - }); - return this; - }, - isActive: function isActive() { - var active; - ttEach(this.first(), function(t) { - active = t.isActive(); - }); - return active; - }, - activate: function activate() { - ttEach(this, function(t) { - t.activate(); - }); - return this; - }, - deactivate: function deactivate() { - ttEach(this, function(t) { - t.deactivate(); - }); - return this; - }, - isOpen: function isOpen() { - var open; - ttEach(this.first(), function(t) { - open = t.isOpen(); - }); - return open; - }, - open: function open() { - ttEach(this, function(t) { - t.open(); - }); - return this; - }, - close: function close() { - ttEach(this, function(t) { - t.close(); - }); - return this; - }, - select: function select(el) { - var success = false, $el = $(el); - ttEach(this.first(), function(t) { - success = t.select($el); - }); - return success; - }, - autocomplete: function autocomplete(el) { - var success = false, $el = $(el); - ttEach(this.first(), function(t) { - success = t.autocomplete($el); - }); - return success; - }, - moveCursor: function moveCursoe(delta) { - var success = false; - ttEach(this.first(), function(t) { - success = t.moveCursor(delta); - }); - return success; - }, - val: function val(newVal) { - var query; - if (!arguments.length) { - ttEach(this.first(), function(t) { - query = t.getVal(); - }); - return query; - } else { - ttEach(this, function(t) { - t.setVal(newVal); - }); - return this; + destroy: function() { + return this.each(destroy); + function destroy() { + var $this = $(this), view = $this.data(viewKey); + if (view) { + view.destroy(); + $this.removeData(viewKey); + } } }, - destroy: function destroy() { - ttEach(this, function(typeahead, $input) { - revert($input); - typeahead.destroy(); - }); - return this; + setQuery: function(query) { + return this.each(setQuery); + function setQuery() { + var view = $(this).data(viewKey); + view && view.setQuery(query); + } } }; - $.fn.typeahead = function(method) { + jQuery.fn.typeahead = function(method) { if (methods[method]) { return methods[method].apply(this, [].slice.call(arguments, 1)); } else { return methods.initialize.apply(this, arguments); } }; - $.fn.typeahead.noConflict = function noConflict() { - $.fn.typeahead = old; - return this; - }; - function ttEach($els, fn) { - $els.each(function() { - var $input = $(this), typeahead; - (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input); - }); - } - function buildHintFromInput($input, www) { - return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop("readonly", true).removeAttr("id name placeholder required").attr({ - autocomplete: "off", - spellcheck: "false", - tabindex: -1 - }); - } - function prepInput($input, www) { - $input.data(keys.attrs, { - dir: $input.attr("dir"), - autocomplete: $input.attr("autocomplete"), - spellcheck: $input.attr("spellcheck"), - style: $input.attr("style") - }); - $input.addClass(www.classes.input).attr({ - autocomplete: "off", - spellcheck: false - }); - try { - !$input.attr("dir") && $input.attr("dir", "auto"); - } catch (e) {} - return $input; - } - function getBackgroundStyles($el) { - return { - backgroundAttachment: $el.css("background-attachment"), - backgroundClip: $el.css("background-clip"), - backgroundColor: $el.css("background-color"), - backgroundImage: $el.css("background-image"), - backgroundOrigin: $el.css("background-origin"), - backgroundPosition: $el.css("background-position"), - backgroundRepeat: $el.css("background-repeat"), - backgroundSize: $el.css("background-size") - }; - } - function revert($input) { - var www, $wrapper; - www = $input.data(keys.www); - $wrapper = $input.parent().filter(www.selectors.wrapper); - _.each($input.data(keys.attrs), function(val, key) { - _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); - }); - $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input); - if ($wrapper.length) { - $input.detach().insertAfter($wrapper); - $wrapper.remove(); - } - } - function $elOrNull(obj) { - var isValid, $el; - isValid = _.isJQuery(obj) || _.isElement(obj); - $el = isValid ? $(obj).first() : []; - return $el.length ? $el : null; - } })(); -}); \ No newline at end of file +})(window.jQuery); diff --git a/app/assets/javascripts/src/Metamaps.js b/app/assets/javascripts/src/Metamaps.js index 5792eb96..af0518d0 100644 --- a/app/assets/javascripts/src/Metamaps.js +++ b/app/assets/javascripts/src/Metamaps.js @@ -669,7 +669,7 @@ Metamaps.Create = { { minLength: 2, }, - { + [{ name: 'topic_autocomplete', limit: 8, template: $('#topicAutocompleteTemplate').html(), @@ -677,7 +677,7 @@ Metamaps.Create = { url: '/topics/autocomplete_topic?term=%QUERY' }, engine: Hogan - } + }] ); // tell the autocomplete to submit the form with the topic you clicked on if you pick from the autocomplete @@ -731,7 +731,7 @@ Metamaps.Create = { { minLength: 2, }, - { + [{ name: 'synapse_autocomplete', template: "
    {{label}}
    ", remote: { @@ -751,7 +751,7 @@ Metamaps.Create = { }, engine: Hogan, header: "

    Existing synapses

    " - } + }] ); $('#synapse_desc').bind('typeahead:selected', function (event, datum, dataset) { From 4bc03e3d2ae02bc54f483cb7bb0e75a5e8de3164 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 1 Oct 2015 12:33:38 +0800 Subject: [PATCH 028/305] code tweaks to searchsynapses --- app/controllers/main_controller.rb | 46 ++++++++++++------------------ 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 3d2a341f..1e8b0c9c 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -68,25 +68,20 @@ class MainController < ApplicationController else search = term.downcase + '%' - if !user - @topics = Topic.where('LOWER("name") like ?', search).where('metacode_id = ?', filterByMetacode.id).order('"name"') - @topics2 = Topic.where('LOWER("name") like ?', '%' + search).where('metacode_id = ?', filterByMetacode.id).order('"name"') - @topics3 = Topic.where('LOWER("desc") like ?', '%' + search).where('metacode_id = ?', filterByMetacode.id).order('"name"') - @topics4 = Topic.where('LOWER("link") like ?', '%' + search).where('metacode_id = ?', filterByMetacode.id).order('"name"') - @topics = @topics + (@topics2 - @topics) - @topics = @topics + (@topics3 - @topics) - @topics = @topics + (@topics4 - @topics) - - elsif user - @topics = Topic.where('LOWER("name") like ?', search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"') - @topics2 = Topic.where('LOWER("name") like ?', '%' + search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"') - @topics3 = Topic.where('LOWER("desc") like ?', '%' + search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"') - @topics4 = Topic.where('LOWER("link") like ?', '%' + search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"') - @topics = @topics + (@topics2 - @topics) - @topics = @topics + (@topics3 - @topics) - @topics = @topics + (@topics4 - @topics) - + if user + @topics = Set.new(Topic.where('LOWER("name") like ?', search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"')) + @topics2 = Set.new(Topic.where('LOWER("name") like ?', '%' + search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"')) + @topics3 = Set.new(Topic.where('LOWER("desc") like ?', '%' + search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"')) + @topics4 = Set.new(Topic.where('LOWER("link") like ?', '%' + search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"')) + else + @topics = Set.new(Topic.where('LOWER("name") like ?', search).where('metacode_id = ?', filterByMetacode.id).order('"name"')) + @topics2 = Set.new(Topic.where('LOWER("name") like ?', '%' + search).where('metacode_id = ?', filterByMetacode.id).order('"name"')) + @topics3 = Set.new(Topic.where('LOWER("desc") like ?', '%' + search).where('metacode_id = ?', filterByMetacode.id).order('"name"')) + @topics4 = Set.new(Topic.where('LOWER("link") like ?', '%' + search).where('metacode_id = ?', filterByMetacode.id).order('"name"')) end + + #get unique elements only through the magic of Sets + @topics = (@topics + @topics2 + @topics3 + @topics4).to_a end elsif desc search = '%' + term.downcase + '%' @@ -211,23 +206,20 @@ class MainController < ApplicationController #limit to 5 results @synapses = @synapses.slice(0,5) - - render json: autocomplete_synapse_generic_json(@synapses) - elsif topic1id && !topic1id.empty? @one = Synapse.where('node1_id = ? AND node2_id = ?', topic1id, topic2id) @two = Synapse.where('node2_id = ? AND node1_id = ?', topic1id, topic2id) @synapses = @one + @two - @synapses.sort! {|s1,s2| s1.desc <=> s2.desc } + @synapses.sort! {|s1,s2| s1.desc <=> s2.desc }.to_a - #read this next line as 'delete a synapse if its private and you're either 1. logged out or 2. logged in but not the synapse creator - @synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } - - render json: autocomplete_synapse_array_json(@synapses) + #permissions + @synapses.delete_if {|s| s.permission == "private" && !authenticated? } + @synapses.delete_if {|s| s.permission == "private" && authenticated? && @current.id != s.user_id } else @synapses = [] - render json: autocomplete_synapse_array_json(@synapses) end + + render json: utocomplete_synapse_array_json(@synapses) end end From bd9c275ada067ab374651ac3e481d4c01f976948 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 1 Oct 2015 14:55:26 +0800 Subject: [PATCH 029/305] upgrade typeahead to 1.10 and new syntax --- .../javascripts/lib/typeahead.bundle.js | 2451 +++++++++++++++++ app/assets/javascripts/lib/typeahead.js | 1165 -------- app/assets/javascripts/src/Metamaps.js | 52 +- 3 files changed, 2481 insertions(+), 1187 deletions(-) create mode 100644 app/assets/javascripts/lib/typeahead.bundle.js delete mode 100644 app/assets/javascripts/lib/typeahead.js diff --git a/app/assets/javascripts/lib/typeahead.bundle.js b/app/assets/javascripts/lib/typeahead.bundle.js new file mode 100644 index 00000000..bb0c8aed --- /dev/null +++ b/app/assets/javascripts/lib/typeahead.bundle.js @@ -0,0 +1,2451 @@ +/*! + * typeahead.js 0.11.1 + * https://github.com/twitter/typeahead.js + * Copyright 2013-2015 Twitter, Inc. and other contributors; Licensed MIT + */ + +(function(root, factory) { + if (typeof define === "function" && define.amd) { + define("bloodhound", [ "jquery" ], function(a0) { + return root["Bloodhound"] = factory(a0); + }); + } else if (typeof exports === "object") { + module.exports = factory(require("jquery")); + } else { + root["Bloodhound"] = factory(jQuery); + } +})(this, function($) { + var _ = function() { + "use strict"; + return { + isMsie: function() { + return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; + }, + isBlankString: function(str) { + return !str || /^\s*$/.test(str); + }, + escapeRegExChars: function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }, + isString: function(obj) { + return typeof obj === "string"; + }, + isNumber: function(obj) { + return typeof obj === "number"; + }, + isArray: $.isArray, + isFunction: $.isFunction, + isObject: $.isPlainObject, + isUndefined: function(obj) { + return typeof obj === "undefined"; + }, + isElement: function(obj) { + return !!(obj && obj.nodeType === 1); + }, + isJQuery: function(obj) { + return obj instanceof $; + }, + toStr: function toStr(s) { + return _.isUndefined(s) || s === null ? "" : s + ""; + }, + bind: $.proxy, + each: function(collection, cb) { + $.each(collection, reverseArgs); + function reverseArgs(index, value) { + return cb(value, index); + } + }, + map: $.map, + filter: $.grep, + every: function(obj, test) { + var result = true; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (!(result = test.call(null, val, key, obj))) { + return false; + } + }); + return !!result; + }, + some: function(obj, test) { + var result = false; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (result = test.call(null, val, key, obj)) { + return false; + } + }); + return !!result; + }, + mixin: $.extend, + identity: function(x) { + return x; + }, + clone: function(obj) { + return $.extend(true, {}, obj); + }, + getIdGenerator: function() { + var counter = 0; + return function() { + return counter++; + }; + }, + templatify: function templatify(obj) { + return $.isFunction(obj) ? obj : template; + function template() { + return String(obj); + } + }, + defer: function(fn) { + setTimeout(fn, 0); + }, + debounce: function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments, later, callNow; + later = function() { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + } + }; + callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + } + return result; + }; + }, + throttle: function(func, wait) { + var context, args, timeout, result, previous, later; + previous = 0; + later = function() { + previous = new Date(); + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = new Date(), remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }, + stringify: function(val) { + return _.isString(val) ? val : JSON.stringify(val); + }, + noop: function() {} + }; + }(); + var VERSION = "0.11.1"; + var tokenizers = function() { + "use strict"; + return { + nonword: nonword, + whitespace: whitespace, + obj: { + nonword: getObjTokenizer(nonword), + whitespace: getObjTokenizer(whitespace) + } + }; + function whitespace(str) { + str = _.toStr(str); + return str ? str.split(/\s+/) : []; + } + function nonword(str) { + str = _.toStr(str); + return str ? str.split(/\W+/) : []; + } + function getObjTokenizer(tokenizer) { + return function setKey(keys) { + keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0); + return function tokenize(o) { + var tokens = []; + _.each(keys, function(k) { + tokens = tokens.concat(tokenizer(_.toStr(o[k]))); + }); + return tokens; + }; + }; + } + }(); + var LruCache = function() { + "use strict"; + function LruCache(maxSize) { + this.maxSize = _.isNumber(maxSize) ? maxSize : 100; + this.reset(); + if (this.maxSize <= 0) { + this.set = this.get = $.noop; + } + } + _.mixin(LruCache.prototype, { + set: function set(key, val) { + var tailItem = this.list.tail, node; + if (this.size >= this.maxSize) { + this.list.remove(tailItem); + delete this.hash[tailItem.key]; + this.size--; + } + if (node = this.hash[key]) { + node.val = val; + this.list.moveToFront(node); + } else { + node = new Node(key, val); + this.list.add(node); + this.hash[key] = node; + this.size++; + } + }, + get: function get(key) { + var node = this.hash[key]; + if (node) { + this.list.moveToFront(node); + return node.val; + } + }, + reset: function reset() { + this.size = 0; + this.hash = {}; + this.list = new List(); + } + }); + function List() { + this.head = this.tail = null; + } + _.mixin(List.prototype, { + add: function add(node) { + if (this.head) { + node.next = this.head; + this.head.prev = node; + } + this.head = node; + this.tail = this.tail || node; + }, + remove: function remove(node) { + node.prev ? node.prev.next = node.next : this.head = node.next; + node.next ? node.next.prev = node.prev : this.tail = node.prev; + }, + moveToFront: function(node) { + this.remove(node); + this.add(node); + } + }); + function Node(key, val) { + this.key = key; + this.val = val; + this.prev = this.next = null; + } + return LruCache; + }(); + var PersistentStorage = function() { + "use strict"; + var LOCAL_STORAGE; + try { + LOCAL_STORAGE = window.localStorage; + LOCAL_STORAGE.setItem("~~~", "!"); + LOCAL_STORAGE.removeItem("~~~"); + } catch (err) { + LOCAL_STORAGE = null; + } + function PersistentStorage(namespace, override) { + this.prefix = [ "__", namespace, "__" ].join(""); + this.ttlKey = "__ttl__"; + this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix)); + this.ls = override || LOCAL_STORAGE; + !this.ls && this._noop(); + } + _.mixin(PersistentStorage.prototype, { + _prefix: function(key) { + return this.prefix + key; + }, + _ttlKey: function(key) { + return this._prefix(key) + this.ttlKey; + }, + _noop: function() { + this.get = this.set = this.remove = this.clear = this.isExpired = _.noop; + }, + _safeSet: function(key, val) { + try { + this.ls.setItem(key, val); + } catch (err) { + if (err.name === "QuotaExceededError") { + this.clear(); + this._noop(); + } + } + }, + get: function(key) { + if (this.isExpired(key)) { + this.remove(key); + } + return decode(this.ls.getItem(this._prefix(key))); + }, + set: function(key, val, ttl) { + if (_.isNumber(ttl)) { + this._safeSet(this._ttlKey(key), encode(now() + ttl)); + } else { + this.ls.removeItem(this._ttlKey(key)); + } + return this._safeSet(this._prefix(key), encode(val)); + }, + remove: function(key) { + this.ls.removeItem(this._ttlKey(key)); + this.ls.removeItem(this._prefix(key)); + return this; + }, + clear: function() { + var i, keys = gatherMatchingKeys(this.keyMatcher); + for (i = keys.length; i--; ) { + this.remove(keys[i]); + } + return this; + }, + isExpired: function(key) { + var ttl = decode(this.ls.getItem(this._ttlKey(key))); + return _.isNumber(ttl) && now() > ttl ? true : false; + } + }); + return PersistentStorage; + function now() { + return new Date().getTime(); + } + function encode(val) { + return JSON.stringify(_.isUndefined(val) ? null : val); + } + function decode(val) { + return $.parseJSON(val); + } + function gatherMatchingKeys(keyMatcher) { + var i, key, keys = [], len = LOCAL_STORAGE.length; + for (i = 0; i < len; i++) { + if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) { + keys.push(key.replace(keyMatcher, "")); + } + } + return keys; + } + }(); + var Transport = function() { + "use strict"; + var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests = 6, sharedCache = new LruCache(10); + function Transport(o) { + o = o || {}; + this.cancelled = false; + this.lastReq = null; + this._send = o.transport; + this._get = o.limiter ? o.limiter(this._get) : this._get; + this._cache = o.cache === false ? new LruCache(0) : sharedCache; + } + Transport.setMaxPendingRequests = function setMaxPendingRequests(num) { + maxPendingRequests = num; + }; + Transport.resetCache = function resetCache() { + sharedCache.reset(); + }; + _.mixin(Transport.prototype, { + _fingerprint: function fingerprint(o) { + o = o || {}; + return o.url + o.type + $.param(o.data || {}); + }, + _get: function(o, cb) { + var that = this, fingerprint, jqXhr; + fingerprint = this._fingerprint(o); + if (this.cancelled || fingerprint !== this.lastReq) { + return; + } + if (jqXhr = pendingRequests[fingerprint]) { + jqXhr.done(done).fail(fail); + } else if (pendingRequestsCount < maxPendingRequests) { + pendingRequestsCount++; + pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always); + } else { + this.onDeckRequestArgs = [].slice.call(arguments, 0); + } + function done(resp) { + cb(null, resp); + that._cache.set(fingerprint, resp); + } + function fail() { + cb(true); + } + function always() { + pendingRequestsCount--; + delete pendingRequests[fingerprint]; + if (that.onDeckRequestArgs) { + that._get.apply(that, that.onDeckRequestArgs); + that.onDeckRequestArgs = null; + } + } + }, + get: function(o, cb) { + var resp, fingerprint; + cb = cb || $.noop; + o = _.isString(o) ? { + url: o + } : o || {}; + fingerprint = this._fingerprint(o); + this.cancelled = false; + this.lastReq = fingerprint; + if (resp = this._cache.get(fingerprint)) { + cb(null, resp); + } else { + this._get(o, cb); + } + }, + cancel: function() { + this.cancelled = true; + } + }); + return Transport; + }(); + var SearchIndex = window.SearchIndex = function() { + "use strict"; + var CHILDREN = "c", IDS = "i"; + function SearchIndex(o) { + o = o || {}; + if (!o.datumTokenizer || !o.queryTokenizer) { + $.error("datumTokenizer and queryTokenizer are both required"); + } + this.identify = o.identify || _.stringify; + this.datumTokenizer = o.datumTokenizer; + this.queryTokenizer = o.queryTokenizer; + this.reset(); + } + _.mixin(SearchIndex.prototype, { + bootstrap: function bootstrap(o) { + this.datums = o.datums; + this.trie = o.trie; + }, + add: function(data) { + var that = this; + data = _.isArray(data) ? data : [ data ]; + _.each(data, function(datum) { + var id, tokens; + that.datums[id = that.identify(datum)] = datum; + tokens = normalizeTokens(that.datumTokenizer(datum)); + _.each(tokens, function(token) { + var node, chars, ch; + node = that.trie; + chars = token.split(""); + while (ch = chars.shift()) { + node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode()); + node[IDS].push(id); + } + }); + }); + }, + get: function get(ids) { + var that = this; + return _.map(ids, function(id) { + return that.datums[id]; + }); + }, + search: function search(query) { + var that = this, tokens, matches; + tokens = normalizeTokens(this.queryTokenizer(query)); + _.each(tokens, function(token) { + var node, chars, ch, ids; + if (matches && matches.length === 0) { + return false; + } + node = that.trie; + chars = token.split(""); + while (node && (ch = chars.shift())) { + node = node[CHILDREN][ch]; + } + if (node && chars.length === 0) { + ids = node[IDS].slice(0); + matches = matches ? getIntersection(matches, ids) : ids; + } else { + matches = []; + return false; + } + }); + return matches ? _.map(unique(matches), function(id) { + return that.datums[id]; + }) : []; + }, + all: function all() { + var values = []; + for (var key in this.datums) { + values.push(this.datums[key]); + } + return values; + }, + reset: function reset() { + this.datums = {}; + this.trie = newNode(); + }, + serialize: function serialize() { + return { + datums: this.datums, + trie: this.trie + }; + } + }); + return SearchIndex; + function normalizeTokens(tokens) { + tokens = _.filter(tokens, function(token) { + return !!token; + }); + tokens = _.map(tokens, function(token) { + return token.toLowerCase(); + }); + return tokens; + } + function newNode() { + var node = {}; + node[IDS] = []; + node[CHILDREN] = {}; + return node; + } + function unique(array) { + var seen = {}, uniques = []; + for (var i = 0, len = array.length; i < len; i++) { + if (!seen[array[i]]) { + seen[array[i]] = true; + uniques.push(array[i]); + } + } + return uniques; + } + function getIntersection(arrayA, arrayB) { + var ai = 0, bi = 0, intersection = []; + arrayA = arrayA.sort(); + arrayB = arrayB.sort(); + var lenArrayA = arrayA.length, lenArrayB = arrayB.length; + while (ai < lenArrayA && bi < lenArrayB) { + if (arrayA[ai] < arrayB[bi]) { + ai++; + } else if (arrayA[ai] > arrayB[bi]) { + bi++; + } else { + intersection.push(arrayA[ai]); + ai++; + bi++; + } + } + return intersection; + } + }(); + var Prefetch = function() { + "use strict"; + var keys; + keys = { + data: "data", + protocol: "protocol", + thumbprint: "thumbprint" + }; + function Prefetch(o) { + this.url = o.url; + this.ttl = o.ttl; + this.cache = o.cache; + this.prepare = o.prepare; + this.transform = o.transform; + this.transport = o.transport; + this.thumbprint = o.thumbprint; + this.storage = new PersistentStorage(o.cacheKey); + } + _.mixin(Prefetch.prototype, { + _settings: function settings() { + return { + url: this.url, + type: "GET", + dataType: "json" + }; + }, + store: function store(data) { + if (!this.cache) { + return; + } + this.storage.set(keys.data, data, this.ttl); + this.storage.set(keys.protocol, location.protocol, this.ttl); + this.storage.set(keys.thumbprint, this.thumbprint, this.ttl); + }, + fromCache: function fromCache() { + var stored = {}, isExpired; + if (!this.cache) { + return null; + } + stored.data = this.storage.get(keys.data); + stored.protocol = this.storage.get(keys.protocol); + stored.thumbprint = this.storage.get(keys.thumbprint); + isExpired = stored.thumbprint !== this.thumbprint || stored.protocol !== location.protocol; + return stored.data && !isExpired ? stored.data : null; + }, + fromNetwork: function(cb) { + var that = this, settings; + if (!cb) { + return; + } + settings = this.prepare(this._settings()); + this.transport(settings).fail(onError).done(onResponse); + function onError() { + cb(true); + } + function onResponse(resp) { + cb(null, that.transform(resp)); + } + }, + clear: function clear() { + this.storage.clear(); + return this; + } + }); + return Prefetch; + }(); + var Remote = function() { + "use strict"; + function Remote(o) { + this.url = o.url; + this.prepare = o.prepare; + this.transform = o.transform; + this.transport = new Transport({ + cache: o.cache, + limiter: o.limiter, + transport: o.transport + }); + } + _.mixin(Remote.prototype, { + _settings: function settings() { + return { + url: this.url, + type: "GET", + dataType: "json" + }; + }, + get: function get(query, cb) { + var that = this, settings; + if (!cb) { + return; + } + query = query || ""; + settings = this.prepare(query, this._settings()); + return this.transport.get(settings, onResponse); + function onResponse(err, resp) { + err ? cb([]) : cb(that.transform(resp)); + } + }, + cancelLastRequest: function cancelLastRequest() { + this.transport.cancel(); + } + }); + return Remote; + }(); + var oParser = function() { + "use strict"; + return function parse(o) { + var defaults, sorter; + defaults = { + initialize: true, + identify: _.stringify, + datumTokenizer: null, + queryTokenizer: null, + sufficient: 5, + sorter: null, + local: [], + prefetch: null, + remote: null + }; + o = _.mixin(defaults, o || {}); + !o.datumTokenizer && $.error("datumTokenizer is required"); + !o.queryTokenizer && $.error("queryTokenizer is required"); + sorter = o.sorter; + o.sorter = sorter ? function(x) { + return x.sort(sorter); + } : _.identity; + o.local = _.isFunction(o.local) ? o.local() : o.local; + o.prefetch = parsePrefetch(o.prefetch); + o.remote = parseRemote(o.remote); + return o; + }; + function parsePrefetch(o) { + var defaults; + if (!o) { + return null; + } + defaults = { + url: null, + ttl: 24 * 60 * 60 * 1e3, + cache: true, + cacheKey: null, + thumbprint: "", + prepare: _.identity, + transform: _.identity, + transport: null + }; + o = _.isString(o) ? { + url: o + } : o; + o = _.mixin(defaults, o); + !o.url && $.error("prefetch requires url to be set"); + o.transform = o.filter || o.transform; + o.cacheKey = o.cacheKey || o.url; + o.thumbprint = VERSION + o.thumbprint; + o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; + return o; + } + function parseRemote(o) { + var defaults; + if (!o) { + return; + } + defaults = { + url: null, + cache: true, + prepare: null, + replace: null, + wildcard: null, + limiter: null, + rateLimitBy: "debounce", + rateLimitWait: 300, + transform: _.identity, + transport: null + }; + o = _.isString(o) ? { + url: o + } : o; + o = _.mixin(defaults, o); + !o.url && $.error("remote requires url to be set"); + o.transform = o.filter || o.transform; + o.prepare = toRemotePrepare(o); + o.limiter = toLimiter(o); + o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax; + delete o.replace; + delete o.wildcard; + delete o.rateLimitBy; + delete o.rateLimitWait; + return o; + } + function toRemotePrepare(o) { + var prepare, replace, wildcard; + prepare = o.prepare; + replace = o.replace; + wildcard = o.wildcard; + if (prepare) { + return prepare; + } + if (replace) { + prepare = prepareByReplace; + } else if (o.wildcard) { + prepare = prepareByWildcard; + } else { + prepare = idenityPrepare; + } + return prepare; + function prepareByReplace(query, settings) { + settings.url = replace(settings.url, query); + return settings; + } + function prepareByWildcard(query, settings) { + settings.url = settings.url.replace(wildcard, encodeURIComponent(query)); + return settings; + } + function idenityPrepare(query, settings) { + return settings; + } + } + function toLimiter(o) { + var limiter, method, wait; + limiter = o.limiter; + method = o.rateLimitBy; + wait = o.rateLimitWait; + if (!limiter) { + limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait); + } + return limiter; + function debounce(wait) { + return function debounce(fn) { + return _.debounce(fn, wait); + }; + } + function throttle(wait) { + return function throttle(fn) { + return _.throttle(fn, wait); + }; + } + } + function callbackToDeferred(fn) { + return function wrapper(o) { + var deferred = $.Deferred(); + fn(o, onSuccess, onError); + return deferred; + function onSuccess(resp) { + _.defer(function() { + deferred.resolve(resp); + }); + } + function onError(err) { + _.defer(function() { + deferred.reject(err); + }); + } + }; + } + }(); + var Bloodhound = function() { + "use strict"; + var old; + old = window && window.Bloodhound; + function Bloodhound(o) { + o = oParser(o); + this.sorter = o.sorter; + this.identify = o.identify; + this.sufficient = o.sufficient; + this.local = o.local; + this.remote = o.remote ? new Remote(o.remote) : null; + this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null; + this.index = new SearchIndex({ + identify: this.identify, + datumTokenizer: o.datumTokenizer, + queryTokenizer: o.queryTokenizer + }); + o.initialize !== false && this.initialize(); + } + Bloodhound.noConflict = function noConflict() { + window && (window.Bloodhound = old); + return Bloodhound; + }; + Bloodhound.tokenizers = tokenizers; + _.mixin(Bloodhound.prototype, { + __ttAdapter: function ttAdapter() { + var that = this; + return this.remote ? withAsync : withoutAsync; + function withAsync(query, sync, async) { + return that.search(query, sync, async); + } + function withoutAsync(query, sync) { + return that.search(query, sync); + } + }, + _loadPrefetch: function loadPrefetch() { + var that = this, deferred, serialized; + deferred = $.Deferred(); + if (!this.prefetch) { + deferred.resolve(); + } else if (serialized = this.prefetch.fromCache()) { + this.index.bootstrap(serialized); + deferred.resolve(); + } else { + this.prefetch.fromNetwork(done); + } + return deferred.promise(); + function done(err, data) { + if (err) { + return deferred.reject(); + } + that.add(data); + that.prefetch.store(that.index.serialize()); + deferred.resolve(); + } + }, + _initialize: function initialize() { + var that = this, deferred; + this.clear(); + (this.initPromise = this._loadPrefetch()).done(addLocalToIndex); + return this.initPromise; + function addLocalToIndex() { + that.add(that.local); + } + }, + initialize: function initialize(force) { + return !this.initPromise || force ? this._initialize() : this.initPromise; + }, + add: function add(data) { + this.index.add(data); + return this; + }, + get: function get(ids) { + ids = _.isArray(ids) ? ids : [].slice.call(arguments); + return this.index.get(ids); + }, + search: function search(query, sync, async) { + var that = this, local; + local = this.sorter(this.index.search(query)); + sync(this.remote ? local.slice() : local); + if (this.remote && local.length < this.sufficient) { + this.remote.get(query, processRemote); + } else if (this.remote) { + this.remote.cancelLastRequest(); + } + return this; + function processRemote(remote) { + var nonDuplicates = []; + _.each(remote, function(r) { + !_.some(local, function(l) { + return that.identify(r) === that.identify(l); + }) && nonDuplicates.push(r); + }); + async && async(nonDuplicates); + } + }, + all: function all() { + return this.index.all(); + }, + clear: function clear() { + this.index.reset(); + return this; + }, + clearPrefetchCache: function clearPrefetchCache() { + this.prefetch && this.prefetch.clear(); + return this; + }, + clearRemoteCache: function clearRemoteCache() { + Transport.resetCache(); + return this; + }, + ttAdapter: function ttAdapter() { + return this.__ttAdapter(); + } + }); + return Bloodhound; + }(); + return Bloodhound; +}); + +(function(root, factory) { + if (typeof define === "function" && define.amd) { + define("typeahead.js", [ "jquery" ], function(a0) { + return factory(a0); + }); + } else if (typeof exports === "object") { + module.exports = factory(require("jquery")); + } else { + factory(jQuery); + } +})(this, function($) { + var _ = function() { + "use strict"; + return { + isMsie: function() { + return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false; + }, + isBlankString: function(str) { + return !str || /^\s*$/.test(str); + }, + escapeRegExChars: function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }, + isString: function(obj) { + return typeof obj === "string"; + }, + isNumber: function(obj) { + return typeof obj === "number"; + }, + isArray: $.isArray, + isFunction: $.isFunction, + isObject: $.isPlainObject, + isUndefined: function(obj) { + return typeof obj === "undefined"; + }, + isElement: function(obj) { + return !!(obj && obj.nodeType === 1); + }, + isJQuery: function(obj) { + return obj instanceof $; + }, + toStr: function toStr(s) { + return _.isUndefined(s) || s === null ? "" : s + ""; + }, + bind: $.proxy, + each: function(collection, cb) { + $.each(collection, reverseArgs); + function reverseArgs(index, value) { + return cb(value, index); + } + }, + map: $.map, + filter: $.grep, + every: function(obj, test) { + var result = true; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (!(result = test.call(null, val, key, obj))) { + return false; + } + }); + return !!result; + }, + some: function(obj, test) { + var result = false; + if (!obj) { + return result; + } + $.each(obj, function(key, val) { + if (result = test.call(null, val, key, obj)) { + return false; + } + }); + return !!result; + }, + mixin: $.extend, + identity: function(x) { + return x; + }, + clone: function(obj) { + return $.extend(true, {}, obj); + }, + getIdGenerator: function() { + var counter = 0; + return function() { + return counter++; + }; + }, + templatify: function templatify(obj) { + return $.isFunction(obj) ? obj : template; + function template() { + return String(obj); + } + }, + defer: function(fn) { + setTimeout(fn, 0); + }, + debounce: function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments, later, callNow; + later = function() { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + } + }; + callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) { + result = func.apply(context, args); + } + return result; + }; + }, + throttle: function(func, wait) { + var context, args, timeout, result, previous, later; + previous = 0; + later = function() { + previous = new Date(); + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = new Date(), remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }, + stringify: function(val) { + return _.isString(val) ? val : JSON.stringify(val); + }, + noop: function() {} + }; + }(); + var WWW = function() { + "use strict"; + var defaultClassNames = { + wrapper: "twitter-typeahead", + input: "tt-input", + hint: "tt-hint", + menu: "tt-menu", + dataset: "tt-dataset", + suggestion: "tt-suggestion", + selectable: "tt-selectable", + empty: "tt-empty", + open: "tt-open", + cursor: "tt-cursor", + highlight: "tt-highlight" + }; + return build; + function build(o) { + var www, classes; + classes = _.mixin({}, defaultClassNames, o); + www = { + css: buildCss(), + classes: classes, + html: buildHtml(classes), + selectors: buildSelectors(classes) + }; + return { + css: www.css, + html: www.html, + classes: www.classes, + selectors: www.selectors, + mixin: function(o) { + _.mixin(o, www); + } + }; + } + function buildHtml(c) { + return { + wrapper: '', + menu: '
    ' + }; + } + function buildSelectors(classes) { + var selectors = {}; + _.each(classes, function(v, k) { + selectors[k] = "." + v; + }); + return selectors; + } + function buildCss() { + var css = { + wrapper: { + position: "relative", + display: "inline-block" + }, + hint: { + position: "absolute", + top: "0", + left: "0", + borderColor: "transparent", + boxShadow: "none", + opacity: "1" + }, + input: { + position: "relative", + verticalAlign: "top", + backgroundColor: "transparent" + }, + inputWithNoHint: { + position: "relative", + verticalAlign: "top" + }, + menu: { + position: "absolute", + top: "100%", + left: "0", + zIndex: "100", + display: "none" + }, + ltr: { + left: "0", + right: "auto" + }, + rtl: { + left: "auto", + right: " 0" + } + }; + if (_.isMsie()) { + _.mixin(css.input, { + backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" + }); + } + return css; + } + }(); + var EventBus = function() { + "use strict"; + var namespace, deprecationMap; + namespace = "typeahead:"; + deprecationMap = { + render: "rendered", + cursorchange: "cursorchanged", + select: "selected", + autocomplete: "autocompleted" + }; + function EventBus(o) { + if (!o || !o.el) { + $.error("EventBus initialized without el"); + } + this.$el = $(o.el); + } + _.mixin(EventBus.prototype, { + _trigger: function(type, args) { + var $e; + $e = $.Event(namespace + type); + (args = args || []).unshift($e); + this.$el.trigger.apply(this.$el, args); + return $e; + }, + before: function(type) { + var args, $e; + args = [].slice.call(arguments, 1); + $e = this._trigger("before" + type, args); + return $e.isDefaultPrevented(); + }, + trigger: function(type) { + var deprecatedType; + this._trigger(type, [].slice.call(arguments, 1)); + if (deprecatedType = deprecationMap[type]) { + this._trigger(deprecatedType, [].slice.call(arguments, 1)); + } + } + }); + return EventBus; + }(); + var EventEmitter = function() { + "use strict"; + var splitter = /\s+/, nextTick = getNextTick(); + return { + onSync: onSync, + onAsync: onAsync, + off: off, + trigger: trigger + }; + function on(method, types, cb, context) { + var type; + if (!cb) { + return this; + } + types = types.split(splitter); + cb = context ? bindContext(cb, context) : cb; + this._callbacks = this._callbacks || {}; + while (type = types.shift()) { + this._callbacks[type] = this._callbacks[type] || { + sync: [], + async: [] + }; + this._callbacks[type][method].push(cb); + } + return this; + } + function onAsync(types, cb, context) { + return on.call(this, "async", types, cb, context); + } + function onSync(types, cb, context) { + return on.call(this, "sync", types, cb, context); + } + function off(types) { + var type; + if (!this._callbacks) { + return this; + } + types = types.split(splitter); + while (type = types.shift()) { + delete this._callbacks[type]; + } + return this; + } + function trigger(types) { + var type, callbacks, args, syncFlush, asyncFlush; + if (!this._callbacks) { + return this; + } + types = types.split(splitter); + args = [].slice.call(arguments, 1); + while ((type = types.shift()) && (callbacks = this._callbacks[type])) { + syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args)); + asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args)); + syncFlush() && nextTick(asyncFlush); + } + return this; + } + function getFlush(callbacks, context, args) { + return flush; + function flush() { + var cancelled; + for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) { + cancelled = callbacks[i].apply(context, args) === false; + } + return !cancelled; + } + } + function getNextTick() { + var nextTickFn; + if (window.setImmediate) { + nextTickFn = function nextTickSetImmediate(fn) { + setImmediate(function() { + fn(); + }); + }; + } else { + nextTickFn = function nextTickSetTimeout(fn) { + setTimeout(function() { + fn(); + }, 0); + }; + } + return nextTickFn; + } + function bindContext(fn, context) { + return fn.bind ? fn.bind(context) : function() { + fn.apply(context, [].slice.call(arguments, 0)); + }; + } + }(); + var highlight = function(doc) { + "use strict"; + var defaults = { + node: null, + pattern: null, + tagName: "strong", + className: null, + wordsOnly: false, + caseSensitive: false + }; + return function hightlight(o) { + var regex; + o = _.mixin({}, defaults, o); + if (!o.node || !o.pattern) { + return; + } + o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ]; + regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly); + traverse(o.node, hightlightTextNode); + function hightlightTextNode(textNode) { + var match, patternNode, wrapperNode; + if (match = regex.exec(textNode.data)) { + wrapperNode = doc.createElement(o.tagName); + o.className && (wrapperNode.className = o.className); + patternNode = textNode.splitText(match.index); + patternNode.splitText(match[0].length); + wrapperNode.appendChild(patternNode.cloneNode(true)); + textNode.parentNode.replaceChild(wrapperNode, patternNode); + } + return !!match; + } + function traverse(el, hightlightTextNode) { + var childNode, TEXT_NODE_TYPE = 3; + for (var i = 0; i < el.childNodes.length; i++) { + childNode = el.childNodes[i]; + if (childNode.nodeType === TEXT_NODE_TYPE) { + i += hightlightTextNode(childNode) ? 1 : 0; + } else { + traverse(childNode, hightlightTextNode); + } + } + } + }; + function getRegex(patterns, caseSensitive, wordsOnly) { + var escapedPatterns = [], regexStr; + for (var i = 0, len = patterns.length; i < len; i++) { + escapedPatterns.push(_.escapeRegExChars(patterns[i])); + } + regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")"; + return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i"); + } + }(window.document); + var Input = function() { + "use strict"; + var specialKeyCodeMap; + specialKeyCodeMap = { + 9: "tab", + 27: "esc", + 37: "left", + 39: "right", + 13: "enter", + 38: "up", + 40: "down" + }; + function Input(o, www) { + o = o || {}; + if (!o.input) { + $.error("input is missing"); + } + www.mixin(this); + this.$hint = $(o.hint); + this.$input = $(o.input); + this.query = this.$input.val(); + this.queryWhenFocused = this.hasFocus() ? this.query : null; + this.$overflowHelper = buildOverflowHelper(this.$input); + this._checkLanguageDirection(); + if (this.$hint.length === 0) { + this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop; + } + } + Input.normalizeQuery = function(str) { + return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " "); + }; + _.mixin(Input.prototype, EventEmitter, { + _onBlur: function onBlur() { + this.resetInputValue(); + this.trigger("blurred"); + }, + _onFocus: function onFocus() { + this.queryWhenFocused = this.query; + this.trigger("focused"); + }, + _onKeydown: function onKeydown($e) { + var keyName = specialKeyCodeMap[$e.which || $e.keyCode]; + this._managePreventDefault(keyName, $e); + if (keyName && this._shouldTrigger(keyName, $e)) { + this.trigger(keyName + "Keyed", $e); + } + }, + _onInput: function onInput() { + this._setQuery(this.getInputValue()); + this.clearHintIfInvalid(); + this._checkLanguageDirection(); + }, + _managePreventDefault: function managePreventDefault(keyName, $e) { + var preventDefault; + switch (keyName) { + case "up": + case "down": + preventDefault = !withModifier($e); + break; + + default: + preventDefault = false; + } + preventDefault && $e.preventDefault(); + }, + _shouldTrigger: function shouldTrigger(keyName, $e) { + var trigger; + switch (keyName) { + case "tab": + trigger = !withModifier($e); + break; + + default: + trigger = true; + } + return trigger; + }, + _checkLanguageDirection: function checkLanguageDirection() { + var dir = (this.$input.css("direction") || "ltr").toLowerCase(); + if (this.dir !== dir) { + this.dir = dir; + this.$hint.attr("dir", dir); + this.trigger("langDirChanged", dir); + } + }, + _setQuery: function setQuery(val, silent) { + var areEquivalent, hasDifferentWhitespace; + areEquivalent = areQueriesEquivalent(val, this.query); + hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false; + this.query = val; + if (!silent && !areEquivalent) { + this.trigger("queryChanged", this.query); + } else if (!silent && hasDifferentWhitespace) { + this.trigger("whitespaceChanged", this.query); + } + }, + bind: function() { + var that = this, onBlur, onFocus, onKeydown, onInput; + onBlur = _.bind(this._onBlur, this); + onFocus = _.bind(this._onFocus, this); + onKeydown = _.bind(this._onKeydown, this); + onInput = _.bind(this._onInput, this); + this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown); + if (!_.isMsie() || _.isMsie() > 9) { + this.$input.on("input.tt", onInput); + } else { + this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { + if (specialKeyCodeMap[$e.which || $e.keyCode]) { + return; + } + _.defer(_.bind(that._onInput, that, $e)); + }); + } + return this; + }, + focus: function focus() { + this.$input.focus(); + }, + blur: function blur() { + this.$input.blur(); + }, + getLangDir: function getLangDir() { + return this.dir; + }, + getQuery: function getQuery() { + return this.query || ""; + }, + setQuery: function setQuery(val, silent) { + this.setInputValue(val); + this._setQuery(val, silent); + }, + hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() { + return this.query !== this.queryWhenFocused; + }, + getInputValue: function getInputValue() { + return this.$input.val(); + }, + setInputValue: function setInputValue(value) { + this.$input.val(value); + this.clearHintIfInvalid(); + this._checkLanguageDirection(); + }, + resetInputValue: function resetInputValue() { + this.setInputValue(this.query); + }, + getHint: function getHint() { + return this.$hint.val(); + }, + setHint: function setHint(value) { + this.$hint.val(value); + }, + clearHint: function clearHint() { + this.setHint(""); + }, + clearHintIfInvalid: function clearHintIfInvalid() { + var val, hint, valIsPrefixOfHint, isValid; + val = this.getInputValue(); + hint = this.getHint(); + valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0; + isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow(); + !isValid && this.clearHint(); + }, + hasFocus: function hasFocus() { + return this.$input.is(":focus"); + }, + hasOverflow: function hasOverflow() { + var constraint = this.$input.width() - 2; + this.$overflowHelper.text(this.getInputValue()); + return this.$overflowHelper.width() >= constraint; + }, + isCursorAtEnd: function() { + var valueLength, selectionStart, range; + valueLength = this.$input.val().length; + selectionStart = this.$input[0].selectionStart; + if (_.isNumber(selectionStart)) { + return selectionStart === valueLength; + } else if (document.selection) { + range = document.selection.createRange(); + range.moveStart("character", -valueLength); + return valueLength === range.text.length; + } + return true; + }, + destroy: function destroy() { + this.$hint.off(".tt"); + this.$input.off(".tt"); + this.$overflowHelper.remove(); + this.$hint = this.$input = this.$overflowHelper = $("
    "); + } + }); + return Input; + function buildOverflowHelper($input) { + return $('').css({ + position: "absolute", + visibility: "hidden", + whiteSpace: "pre", + fontFamily: $input.css("font-family"), + fontSize: $input.css("font-size"), + fontStyle: $input.css("font-style"), + fontVariant: $input.css("font-variant"), + fontWeight: $input.css("font-weight"), + wordSpacing: $input.css("word-spacing"), + letterSpacing: $input.css("letter-spacing"), + textIndent: $input.css("text-indent"), + textRendering: $input.css("text-rendering"), + textTransform: $input.css("text-transform") + }).insertAfter($input); + } + function areQueriesEquivalent(a, b) { + return Input.normalizeQuery(a) === Input.normalizeQuery(b); + } + function withModifier($e) { + return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey; + } + }(); + var Dataset = function() { + "use strict"; + var keys, nameGenerator; + keys = { + val: "tt-selectable-display", + obj: "tt-selectable-object" + }; + nameGenerator = _.getIdGenerator(); + function Dataset(o, www) { + o = o || {}; + o.templates = o.templates || {}; + o.templates.notFound = o.templates.notFound || o.templates.empty; + if (!o.source) { + $.error("missing source"); + } + if (!o.node) { + $.error("missing node"); + } + if (o.name && !isValidName(o.name)) { + $.error("invalid dataset name: " + o.name); + } + www.mixin(this); + this.highlight = !!o.highlight; + this.name = o.name || nameGenerator(); + this.limit = o.limit || 5; + this.displayFn = getDisplayFn(o.display || o.displayKey); + this.templates = getTemplates(o.templates, this.displayFn); + this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source; + this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async; + this._resetLastSuggestion(); + this.$el = $(o.node).addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name); + } + Dataset.extractData = function extractData(el) { + var $el = $(el); + if ($el.data(keys.obj)) { + return { + val: $el.data(keys.val) || "", + obj: $el.data(keys.obj) || null + }; + } + return null; + }; + _.mixin(Dataset.prototype, EventEmitter, { + _overwrite: function overwrite(query, suggestions) { + suggestions = suggestions || []; + if (suggestions.length) { + this._renderSuggestions(query, suggestions); + } else if (this.async && this.templates.pending) { + this._renderPending(query); + } else if (!this.async && this.templates.notFound) { + this._renderNotFound(query); + } else { + this._empty(); + } + this.trigger("rendered", this.name, suggestions, false); + }, + _append: function append(query, suggestions) { + suggestions = suggestions || []; + if (suggestions.length && this.$lastSuggestion.length) { + this._appendSuggestions(query, suggestions); + } else if (suggestions.length) { + this._renderSuggestions(query, suggestions); + } else if (!this.$lastSuggestion.length && this.templates.notFound) { + this._renderNotFound(query); + } + this.trigger("rendered", this.name, suggestions, true); + }, + _renderSuggestions: function renderSuggestions(query, suggestions) { + var $fragment; + $fragment = this._getSuggestionsFragment(query, suggestions); + this.$lastSuggestion = $fragment.children().last(); + this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions)); + }, + _appendSuggestions: function appendSuggestions(query, suggestions) { + var $fragment, $lastSuggestion; + $fragment = this._getSuggestionsFragment(query, suggestions); + $lastSuggestion = $fragment.children().last(); + this.$lastSuggestion.after($fragment); + this.$lastSuggestion = $lastSuggestion; + }, + _renderPending: function renderPending(query) { + var template = this.templates.pending; + this._resetLastSuggestion(); + template && this.$el.html(template({ + query: query, + dataset: this.name + })); + }, + _renderNotFound: function renderNotFound(query) { + var template = this.templates.notFound; + this._resetLastSuggestion(); + template && this.$el.html(template({ + query: query, + dataset: this.name + })); + }, + _empty: function empty() { + this.$el.empty(); + this._resetLastSuggestion(); + }, + _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) { + var that = this, fragment; + fragment = document.createDocumentFragment(); + _.each(suggestions, function getSuggestionNode(suggestion) { + var $el, context; + context = that._injectQuery(query, suggestion); + $el = $(that.templates.suggestion(context)).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable); + fragment.appendChild($el[0]); + }); + this.highlight && highlight({ + className: this.classes.highlight, + node: fragment, + pattern: query + }); + return $(fragment); + }, + _getFooter: function getFooter(query, suggestions) { + return this.templates.footer ? this.templates.footer({ + query: query, + suggestions: suggestions, + dataset: this.name + }) : null; + }, + _getHeader: function getHeader(query, suggestions) { + return this.templates.header ? this.templates.header({ + query: query, + suggestions: suggestions, + dataset: this.name + }) : null; + }, + _resetLastSuggestion: function resetLastSuggestion() { + this.$lastSuggestion = $(); + }, + _injectQuery: function injectQuery(query, obj) { + return _.isObject(obj) ? _.mixin({ + _query: query + }, obj) : obj; + }, + update: function update(query) { + var that = this, canceled = false, syncCalled = false, rendered = 0; + this.cancel(); + this.cancel = function cancel() { + canceled = true; + that.cancel = $.noop; + that.async && that.trigger("asyncCanceled", query); + }; + this.source(query, sync, async); + !syncCalled && sync([]); + function sync(suggestions) { + if (syncCalled) { + return; + } + syncCalled = true; + suggestions = (suggestions || []).slice(0, that.limit); + rendered = suggestions.length; + that._overwrite(query, suggestions); + if (rendered < that.limit && that.async) { + that.trigger("asyncRequested", query); + } + } + function async(suggestions) { + suggestions = suggestions || []; + if (!canceled && rendered < that.limit) { + that.cancel = $.noop; + rendered += suggestions.length; + that._append(query, suggestions.slice(0, that.limit - rendered)); + that.async && that.trigger("asyncReceived", query); + } + } + }, + cancel: $.noop, + clear: function clear() { + this._empty(); + this.cancel(); + this.trigger("cleared"); + }, + isEmpty: function isEmpty() { + return this.$el.is(":empty"); + }, + destroy: function destroy() { + this.$el = $("
    "); + } + }); + return Dataset; + function getDisplayFn(display) { + display = display || _.stringify; + return _.isFunction(display) ? display : displayFn; + function displayFn(obj) { + return obj[display]; + } + } + function getTemplates(templates, displayFn) { + return { + notFound: templates.notFound && _.templatify(templates.notFound), + pending: templates.pending && _.templatify(templates.pending), + header: templates.header && _.templatify(templates.header), + footer: templates.footer && _.templatify(templates.footer), + suggestion: templates.suggestion || suggestionTemplate + }; + function suggestionTemplate(context) { + return $("
    ").text(displayFn(context)); + } + } + function isValidName(str) { + return /^[_a-zA-Z0-9-]+$/.test(str); + } + }(); + var Menu = function() { + "use strict"; + function Menu(o, www) { + var that = this; + o = o || {}; + if (!o.node) { + $.error("node is required"); + } + www.mixin(this); + this.$node = $(o.node); + this.query = null; + this.datasets = _.map(o.datasets, initializeDataset); + function initializeDataset(oDataset) { + var node = that.$node.find(oDataset.node).first(); + oDataset.node = node.length ? node : $("
    ").appendTo(that.$node); + return new Dataset(oDataset, www); + } + } + _.mixin(Menu.prototype, EventEmitter, { + _onSelectableClick: function onSelectableClick($e) { + this.trigger("selectableClicked", $($e.currentTarget)); + }, + _onRendered: function onRendered(type, dataset, suggestions, async) { + this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); + this.trigger("datasetRendered", dataset, suggestions, async); + }, + _onCleared: function onCleared() { + this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty()); + this.trigger("datasetCleared"); + }, + _propagate: function propagate() { + this.trigger.apply(this, arguments); + }, + _allDatasetsEmpty: function allDatasetsEmpty() { + return _.every(this.datasets, isDatasetEmpty); + function isDatasetEmpty(dataset) { + return dataset.isEmpty(); + } + }, + _getSelectables: function getSelectables() { + return this.$node.find(this.selectors.selectable); + }, + _removeCursor: function _removeCursor() { + var $selectable = this.getActiveSelectable(); + $selectable && $selectable.removeClass(this.classes.cursor); + }, + _ensureVisible: function ensureVisible($el) { + var elTop, elBottom, nodeScrollTop, nodeHeight; + elTop = $el.position().top; + elBottom = elTop + $el.outerHeight(true); + nodeScrollTop = this.$node.scrollTop(); + nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10); + if (elTop < 0) { + this.$node.scrollTop(nodeScrollTop + elTop); + } else if (nodeHeight < elBottom) { + this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight)); + } + }, + bind: function() { + var that = this, onSelectableClick; + onSelectableClick = _.bind(this._onSelectableClick, this); + this.$node.on("click.tt", this.selectors.selectable, onSelectableClick); + _.each(this.datasets, function(dataset) { + dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that); + }); + return this; + }, + isOpen: function isOpen() { + return this.$node.hasClass(this.classes.open); + }, + open: function open() { + this.$node.addClass(this.classes.open); + }, + close: function close() { + this.$node.removeClass(this.classes.open); + this._removeCursor(); + }, + setLanguageDirection: function setLanguageDirection(dir) { + this.$node.attr("dir", dir); + }, + selectableRelativeToCursor: function selectableRelativeToCursor(delta) { + var $selectables, $oldCursor, oldIndex, newIndex; + $oldCursor = this.getActiveSelectable(); + $selectables = this._getSelectables(); + oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1; + newIndex = oldIndex + delta; + newIndex = (newIndex + 1) % ($selectables.length + 1) - 1; + newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex; + return newIndex === -1 ? null : $selectables.eq(newIndex); + }, + setCursor: function setCursor($selectable) { + this._removeCursor(); + if ($selectable = $selectable && $selectable.first()) { + $selectable.addClass(this.classes.cursor); + this._ensureVisible($selectable); + } + }, + getSelectableData: function getSelectableData($el) { + return $el && $el.length ? Dataset.extractData($el) : null; + }, + getActiveSelectable: function getActiveSelectable() { + var $selectable = this._getSelectables().filter(this.selectors.cursor).first(); + return $selectable.length ? $selectable : null; + }, + getTopSelectable: function getTopSelectable() { + var $selectable = this._getSelectables().first(); + return $selectable.length ? $selectable : null; + }, + update: function update(query) { + var isValidUpdate = query !== this.query; + if (isValidUpdate) { + this.query = query; + _.each(this.datasets, updateDataset); + } + return isValidUpdate; + function updateDataset(dataset) { + dataset.update(query); + } + }, + empty: function empty() { + _.each(this.datasets, clearDataset); + this.query = null; + this.$node.addClass(this.classes.empty); + function clearDataset(dataset) { + dataset.clear(); + } + }, + destroy: function destroy() { + this.$node.off(".tt"); + this.$node = $("
    "); + _.each(this.datasets, destroyDataset); + function destroyDataset(dataset) { + dataset.destroy(); + } + } + }); + return Menu; + }(); + var DefaultMenu = function() { + "use strict"; + var s = Menu.prototype; + function DefaultMenu() { + Menu.apply(this, [].slice.call(arguments, 0)); + } + _.mixin(DefaultMenu.prototype, Menu.prototype, { + open: function open() { + !this._allDatasetsEmpty() && this._show(); + return s.open.apply(this, [].slice.call(arguments, 0)); + }, + close: function close() { + this._hide(); + return s.close.apply(this, [].slice.call(arguments, 0)); + }, + _onRendered: function onRendered() { + if (this._allDatasetsEmpty()) { + this._hide(); + } else { + this.isOpen() && this._show(); + } + return s._onRendered.apply(this, [].slice.call(arguments, 0)); + }, + _onCleared: function onCleared() { + if (this._allDatasetsEmpty()) { + this._hide(); + } else { + this.isOpen() && this._show(); + } + return s._onCleared.apply(this, [].slice.call(arguments, 0)); + }, + setLanguageDirection: function setLanguageDirection(dir) { + this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl); + return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0)); + }, + _hide: function hide() { + this.$node.hide(); + }, + _show: function show() { + this.$node.css("display", "block"); + } + }); + return DefaultMenu; + }(); + var Typeahead = function() { + "use strict"; + function Typeahead(o, www) { + var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged; + o = o || {}; + if (!o.input) { + $.error("missing input"); + } + if (!o.menu) { + $.error("missing menu"); + } + if (!o.eventBus) { + $.error("missing event bus"); + } + www.mixin(this); + this.eventBus = o.eventBus; + this.minLength = _.isNumber(o.minLength) ? o.minLength : 1; + this.input = o.input; + this.menu = o.menu; + this.enabled = true; + this.active = false; + this.input.hasFocus() && this.activate(); + this.dir = this.input.getLangDir(); + this._hacks(); + this.menu.bind().onSync("selectableClicked", this._onSelectableClicked, this).onSync("asyncRequested", this._onAsyncRequested, this).onSync("asyncCanceled", this._onAsyncCanceled, this).onSync("asyncReceived", this._onAsyncReceived, this).onSync("datasetRendered", this._onDatasetRendered, this).onSync("datasetCleared", this._onDatasetCleared, this); + onFocused = c(this, "activate", "open", "_onFocused"); + onBlurred = c(this, "deactivate", "_onBlurred"); + onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed"); + onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed"); + onEscKeyed = c(this, "isActive", "_onEscKeyed"); + onUpKeyed = c(this, "isActive", "open", "_onUpKeyed"); + onDownKeyed = c(this, "isActive", "open", "_onDownKeyed"); + onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed"); + onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed"); + onQueryChanged = c(this, "_openIfActive", "_onQueryChanged"); + onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged"); + this.input.bind().onSync("focused", onFocused, this).onSync("blurred", onBlurred, this).onSync("enterKeyed", onEnterKeyed, this).onSync("tabKeyed", onTabKeyed, this).onSync("escKeyed", onEscKeyed, this).onSync("upKeyed", onUpKeyed, this).onSync("downKeyed", onDownKeyed, this).onSync("leftKeyed", onLeftKeyed, this).onSync("rightKeyed", onRightKeyed, this).onSync("queryChanged", onQueryChanged, this).onSync("whitespaceChanged", onWhitespaceChanged, this).onSync("langDirChanged", this._onLangDirChanged, this); + } + _.mixin(Typeahead.prototype, { + _hacks: function hacks() { + var $input, $menu; + $input = this.input.$input || $("
    "); + $menu = this.menu.$node || $("
    "); + $input.on("blur.tt", function($e) { + var active, isActive, hasActive; + active = document.activeElement; + isActive = $menu.is(active); + hasActive = $menu.has(active).length > 0; + if (_.isMsie() && (isActive || hasActive)) { + $e.preventDefault(); + $e.stopImmediatePropagation(); + _.defer(function() { + $input.focus(); + }); + } + }); + $menu.on("mousedown.tt", function($e) { + $e.preventDefault(); + }); + }, + _onSelectableClicked: function onSelectableClicked(type, $el) { + this.select($el); + }, + _onDatasetCleared: function onDatasetCleared() { + this._updateHint(); + }, + _onDatasetRendered: function onDatasetRendered(type, dataset, suggestions, async) { + this._updateHint(); + this.eventBus.trigger("render", suggestions, async, dataset); + }, + _onAsyncRequested: function onAsyncRequested(type, dataset, query) { + this.eventBus.trigger("asyncrequest", query, dataset); + }, + _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) { + this.eventBus.trigger("asynccancel", query, dataset); + }, + _onAsyncReceived: function onAsyncReceived(type, dataset, query) { + this.eventBus.trigger("asyncreceive", query, dataset); + }, + _onFocused: function onFocused() { + this._minLengthMet() && this.menu.update(this.input.getQuery()); + }, + _onBlurred: function onBlurred() { + if (this.input.hasQueryChangedSinceLastFocus()) { + this.eventBus.trigger("change", this.input.getQuery()); + } + }, + _onEnterKeyed: function onEnterKeyed(type, $e) { + var $selectable; + if ($selectable = this.menu.getActiveSelectable()) { + this.select($selectable) && $e.preventDefault(); + } + }, + _onTabKeyed: function onTabKeyed(type, $e) { + var $selectable; + if ($selectable = this.menu.getActiveSelectable()) { + this.select($selectable) && $e.preventDefault(); + } else if ($selectable = this.menu.getTopSelectable()) { + this.autocomplete($selectable) && $e.preventDefault(); + } + }, + _onEscKeyed: function onEscKeyed() { + this.close(); + }, + _onUpKeyed: function onUpKeyed() { + this.moveCursor(-1); + }, + _onDownKeyed: function onDownKeyed() { + this.moveCursor(+1); + }, + _onLeftKeyed: function onLeftKeyed() { + if (this.dir === "rtl" && this.input.isCursorAtEnd()) { + this.autocomplete(this.menu.getTopSelectable()); + } + }, + _onRightKeyed: function onRightKeyed() { + if (this.dir === "ltr" && this.input.isCursorAtEnd()) { + this.autocomplete(this.menu.getTopSelectable()); + } + }, + _onQueryChanged: function onQueryChanged(e, query) { + this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty(); + }, + _onWhitespaceChanged: function onWhitespaceChanged() { + this._updateHint(); + }, + _onLangDirChanged: function onLangDirChanged(e, dir) { + if (this.dir !== dir) { + this.dir = dir; + this.menu.setLanguageDirection(dir); + } + }, + _openIfActive: function openIfActive() { + this.isActive() && this.open(); + }, + _minLengthMet: function minLengthMet(query) { + query = _.isString(query) ? query : this.input.getQuery() || ""; + return query.length >= this.minLength; + }, + _updateHint: function updateHint() { + var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match; + $selectable = this.menu.getTopSelectable(); + data = this.menu.getSelectableData($selectable); + val = this.input.getInputValue(); + if (data && !_.isBlankString(val) && !this.input.hasOverflow()) { + query = Input.normalizeQuery(val); + escapedQuery = _.escapeRegExChars(query); + frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i"); + match = frontMatchRegEx.exec(data.val); + match && this.input.setHint(val + match[1]); + } else { + this.input.clearHint(); + } + }, + isEnabled: function isEnabled() { + return this.enabled; + }, + enable: function enable() { + this.enabled = true; + }, + disable: function disable() { + this.enabled = false; + }, + isActive: function isActive() { + return this.active; + }, + activate: function activate() { + if (this.isActive()) { + return true; + } else if (!this.isEnabled() || this.eventBus.before("active")) { + return false; + } else { + this.active = true; + this.eventBus.trigger("active"); + return true; + } + }, + deactivate: function deactivate() { + if (!this.isActive()) { + return true; + } else if (this.eventBus.before("idle")) { + return false; + } else { + this.active = false; + this.close(); + this.eventBus.trigger("idle"); + return true; + } + }, + isOpen: function isOpen() { + return this.menu.isOpen(); + }, + open: function open() { + if (!this.isOpen() && !this.eventBus.before("open")) { + this.menu.open(); + this._updateHint(); + this.eventBus.trigger("open"); + } + return this.isOpen(); + }, + close: function close() { + if (this.isOpen() && !this.eventBus.before("close")) { + this.menu.close(); + this.input.clearHint(); + this.input.resetInputValue(); + this.eventBus.trigger("close"); + } + return !this.isOpen(); + }, + setVal: function setVal(val) { + this.input.setQuery(_.toStr(val)); + }, + getVal: function getVal() { + return this.input.getQuery(); + }, + select: function select($selectable) { + var data = this.menu.getSelectableData($selectable); + if (data && !this.eventBus.before("select", data.obj)) { + this.input.setQuery(data.val, true); + this.eventBus.trigger("select", data.obj); + this.close(); + return true; + } + return false; + }, + autocomplete: function autocomplete($selectable) { + var query, data, isValid; + query = this.input.getQuery(); + data = this.menu.getSelectableData($selectable); + isValid = data && query !== data.val; + if (isValid && !this.eventBus.before("autocomplete", data.obj)) { + this.input.setQuery(data.val); + this.eventBus.trigger("autocomplete", data.obj); + return true; + } + return false; + }, + moveCursor: function moveCursor(delta) { + var query, $candidate, data, payload, cancelMove; + query = this.input.getQuery(); + $candidate = this.menu.selectableRelativeToCursor(delta); + data = this.menu.getSelectableData($candidate); + payload = data ? data.obj : null; + cancelMove = this._minLengthMet() && this.menu.update(query); + if (!cancelMove && !this.eventBus.before("cursorchange", payload)) { + this.menu.setCursor($candidate); + if (data) { + this.input.setInputValue(data.val); + } else { + this.input.resetInputValue(); + this._updateHint(); + } + this.eventBus.trigger("cursorchange", payload); + return true; + } + return false; + }, + destroy: function destroy() { + this.input.destroy(); + this.menu.destroy(); + } + }); + return Typeahead; + function c(ctx) { + var methods = [].slice.call(arguments, 1); + return function() { + var args = [].slice.call(arguments); + _.each(methods, function(method) { + return ctx[method].apply(ctx, args); + }); + }; + } + }(); + (function() { + "use strict"; + var old, keys, methods; + old = $.fn.typeahead; + keys = { + www: "tt-www", + attrs: "tt-attrs", + typeahead: "tt-typeahead" + }; + methods = { + initialize: function initialize(o, datasets) { + var www; + datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1); + o = o || {}; + www = WWW(o.classNames); + return this.each(attach); + function attach() { + var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, typeahead, MenuConstructor; + _.each(datasets, function(d) { + d.highlight = !!o.highlight; + }); + $input = $(this); + $wrapper = $(www.html.wrapper); + $hint = $elOrNull(o.hint); + $menu = $elOrNull(o.menu); + defaultHint = o.hint !== false && !$hint; + defaultMenu = o.menu !== false && !$menu; + defaultHint && ($hint = buildHintFromInput($input, www)); + defaultMenu && ($menu = $(www.html.menu).css(www.css.menu)); + $hint && $hint.val(""); + $input = prepInput($input, www); + if (defaultHint || defaultMenu) { + $wrapper.css(www.css.wrapper); + $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint); + $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null); + } + MenuConstructor = defaultMenu ? DefaultMenu : Menu; + eventBus = new EventBus({ + el: $input + }); + input = new Input({ + hint: $hint, + input: $input + }, www); + menu = new MenuConstructor({ + node: $menu, + datasets: datasets + }, www); + typeahead = new Typeahead({ + input: input, + menu: menu, + eventBus: eventBus, + minLength: o.minLength + }, www); + $input.data(keys.www, www); + $input.data(keys.typeahead, typeahead); + } + }, + isEnabled: function isEnabled() { + var enabled; + ttEach(this.first(), function(t) { + enabled = t.isEnabled(); + }); + return enabled; + }, + enable: function enable() { + ttEach(this, function(t) { + t.enable(); + }); + return this; + }, + disable: function disable() { + ttEach(this, function(t) { + t.disable(); + }); + return this; + }, + isActive: function isActive() { + var active; + ttEach(this.first(), function(t) { + active = t.isActive(); + }); + return active; + }, + activate: function activate() { + ttEach(this, function(t) { + t.activate(); + }); + return this; + }, + deactivate: function deactivate() { + ttEach(this, function(t) { + t.deactivate(); + }); + return this; + }, + isOpen: function isOpen() { + var open; + ttEach(this.first(), function(t) { + open = t.isOpen(); + }); + return open; + }, + open: function open() { + ttEach(this, function(t) { + t.open(); + }); + return this; + }, + close: function close() { + ttEach(this, function(t) { + t.close(); + }); + return this; + }, + select: function select(el) { + var success = false, $el = $(el); + ttEach(this.first(), function(t) { + success = t.select($el); + }); + return success; + }, + autocomplete: function autocomplete(el) { + var success = false, $el = $(el); + ttEach(this.first(), function(t) { + success = t.autocomplete($el); + }); + return success; + }, + moveCursor: function moveCursoe(delta) { + var success = false; + ttEach(this.first(), function(t) { + success = t.moveCursor(delta); + }); + return success; + }, + val: function val(newVal) { + var query; + if (!arguments.length) { + ttEach(this.first(), function(t) { + query = t.getVal(); + }); + return query; + } else { + ttEach(this, function(t) { + t.setVal(newVal); + }); + return this; + } + }, + destroy: function destroy() { + ttEach(this, function(typeahead, $input) { + revert($input); + typeahead.destroy(); + }); + return this; + } + }; + $.fn.typeahead = function(method) { + if (methods[method]) { + return methods[method].apply(this, [].slice.call(arguments, 1)); + } else { + return methods.initialize.apply(this, arguments); + } + }; + $.fn.typeahead.noConflict = function noConflict() { + $.fn.typeahead = old; + return this; + }; + function ttEach($els, fn) { + $els.each(function() { + var $input = $(this), typeahead; + (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input); + }); + } + function buildHintFromInput($input, www) { + return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop("readonly", true).removeAttr("id name placeholder required").attr({ + autocomplete: "off", + spellcheck: "false", + tabindex: -1 + }); + } + function prepInput($input, www) { + $input.data(keys.attrs, { + dir: $input.attr("dir"), + autocomplete: $input.attr("autocomplete"), + spellcheck: $input.attr("spellcheck"), + style: $input.attr("style") + }); + $input.addClass(www.classes.input).attr({ + autocomplete: "off", + spellcheck: false + }); + try { + !$input.attr("dir") && $input.attr("dir", "auto"); + } catch (e) {} + return $input; + } + function getBackgroundStyles($el) { + return { + backgroundAttachment: $el.css("background-attachment"), + backgroundClip: $el.css("background-clip"), + backgroundColor: $el.css("background-color"), + backgroundImage: $el.css("background-image"), + backgroundOrigin: $el.css("background-origin"), + backgroundPosition: $el.css("background-position"), + backgroundRepeat: $el.css("background-repeat"), + backgroundSize: $el.css("background-size") + }; + } + function revert($input) { + var www, $wrapper; + www = $input.data(keys.www); + $wrapper = $input.parent().filter(www.selectors.wrapper); + _.each($input.data(keys.attrs), function(val, key) { + _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); + }); + $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input); + if ($wrapper.length) { + $input.detach().insertAfter($wrapper); + $wrapper.remove(); + } + } + function $elOrNull(obj) { + var isValid, $el; + isValid = _.isJQuery(obj) || _.isElement(obj); + $el = isValid ? $(obj).first() : []; + return $el.length ? $el : null; + } + })(); +}); \ No newline at end of file diff --git a/app/assets/javascripts/lib/typeahead.js b/app/assets/javascripts/lib/typeahead.js deleted file mode 100644 index 3a413d68..00000000 --- a/app/assets/javascripts/lib/typeahead.js +++ /dev/null @@ -1,1165 +0,0 @@ -/*! - * typeahead.js 0.9.3 - * https://github.com/twitter/typeahead - * Copyright 2013 Twitter, Inc. and other contributors; Licensed MIT - */ - -(function($) { - var VERSION = "0.9.3"; - var utils = { - isMsie: function() { - var match = /(msie) ([\w.]+)/i.exec(navigator.userAgent); - return match ? parseInt(match[2], 10) : false; - }, - isBlankString: function(str) { - return !str || /^\s*$/.test(str); - }, - escapeRegExChars: function(str) { - return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); - }, - isString: function(obj) { - return typeof obj === "string"; - }, - isNumber: function(obj) { - return typeof obj === "number"; - }, - isArray: $.isArray, - isFunction: $.isFunction, - isObject: $.isPlainObject, - isUndefined: function(obj) { - return typeof obj === "undefined"; - }, - bind: $.proxy, - bindAll: function(obj) { - var val; - for (var key in obj) { - $.isFunction(val = obj[key]) && (obj[key] = $.proxy(val, obj)); - } - }, - indexOf: function(haystack, needle) { - for (var i = 0; i < haystack.length; i++) { - if (haystack[i] === needle) { - return i; - } - } - return -1; - }, - each: $.each, - map: $.map, - filter: $.grep, - every: function(obj, test) { - var result = true; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (!(result = test.call(null, val, key, obj))) { - return false; - } - }); - return !!result; - }, - some: function(obj, test) { - var result = false; - if (!obj) { - return result; - } - $.each(obj, function(key, val) { - if (result = test.call(null, val, key, obj)) { - return false; - } - }); - return !!result; - }, - mixin: $.extend, - getUniqueId: function() { - var counter = 0; - return function() { - return counter++; - }; - }(), - defer: function(fn) { - setTimeout(fn, 0); - }, - debounce: function(func, wait, immediate) { - var timeout, result; - return function() { - var context = this, args = arguments, later, callNow; - later = function() { - timeout = null; - if (!immediate) { - result = func.apply(context, args); - } - }; - callNow = immediate && !timeout; - clearTimeout(timeout); - timeout = setTimeout(later, wait); - if (callNow) { - result = func.apply(context, args); - } - return result; - }; - }, - throttle: function(func, wait) { - var context, args, timeout, result, previous, later; - previous = 0; - later = function() { - previous = new Date(); - timeout = null; - result = func.apply(context, args); - }; - return function() { - var now = new Date(), remaining = wait - (now - previous); - context = this; - args = arguments; - if (remaining <= 0) { - clearTimeout(timeout); - timeout = null; - previous = now; - result = func.apply(context, args); - } else if (!timeout) { - timeout = setTimeout(later, remaining); - } - return result; - }; - }, - tokenizeQuery: function(str) { - return $.trim(str).toLowerCase().split(/[\s]+/); - }, - tokenizeText: function(str) { - return $.trim(str).toLowerCase().split(/[\s\-_]+/); - }, - getProtocol: function() { - return location.protocol; - }, - noop: function() {} - }; - var EventTarget = function() { - var eventSplitter = /\s+/; - return { - on: function(events, callback) { - var event; - if (!callback) { - return this; - } - this._callbacks = this._callbacks || {}; - events = events.split(eventSplitter); - while (event = events.shift()) { - this._callbacks[event] = this._callbacks[event] || []; - this._callbacks[event].push(callback); - } - return this; - }, - trigger: function(events, data) { - var event, callbacks; - if (!this._callbacks) { - return this; - } - events = events.split(eventSplitter); - while (event = events.shift()) { - if (callbacks = this._callbacks[event]) { - for (var i = 0; i < callbacks.length; i += 1) { - callbacks[i].call(this, { - type: event, - data: data - }); - } - } - } - return this; - } - }; - }(); - var EventBus = function() { - var namespace = "typeahead:"; - function EventBus(o) { - if (!o || !o.el) { - $.error("EventBus initialized without el"); - } - this.$el = $(o.el); - } - utils.mixin(EventBus.prototype, { - trigger: function(type) { - var args = [].slice.call(arguments, 1); - this.$el.trigger(namespace + type, args); - } - }); - return EventBus; - }(); - var PersistentStorage = function() { - var ls, methods; - try { - ls = window.localStorage; - ls.setItem("~~~", "!"); - ls.removeItem("~~~"); - } catch (err) { - ls = null; - } - function PersistentStorage(namespace) { - this.prefix = [ "__", namespace, "__" ].join(""); - this.ttlKey = "__ttl__"; - this.keyMatcher = new RegExp("^" + this.prefix); - } - if (ls && window.JSON) { - methods = { - _prefix: function(key) { - return this.prefix + key; - }, - _ttlKey: function(key) { - return this._prefix(key) + this.ttlKey; - }, - get: function(key) { - if (this.isExpired(key)) { - this.remove(key); - } - return decode(ls.getItem(this._prefix(key))); - }, - set: function(key, val, ttl) { - if (utils.isNumber(ttl)) { - ls.setItem(this._ttlKey(key), encode(now() + ttl)); - } else { - ls.removeItem(this._ttlKey(key)); - } - return ls.setItem(this._prefix(key), encode(val)); - }, - remove: function(key) { - ls.removeItem(this._ttlKey(key)); - ls.removeItem(this._prefix(key)); - return this; - }, - clear: function() { - var i, key, keys = [], len = ls.length; - for (i = 0; i < len; i++) { - if ((key = ls.key(i)).match(this.keyMatcher)) { - keys.push(key.replace(this.keyMatcher, "")); - } - } - for (i = keys.length; i--; ) { - this.remove(keys[i]); - } - return this; - }, - isExpired: function(key) { - var ttl = decode(ls.getItem(this._ttlKey(key))); - return utils.isNumber(ttl) && now() > ttl ? true : false; - } - }; - } else { - methods = { - get: utils.noop, - set: utils.noop, - remove: utils.noop, - clear: utils.noop, - isExpired: utils.noop - }; - } - utils.mixin(PersistentStorage.prototype, methods); - return PersistentStorage; - function now() { - return new Date().getTime(); - } - function encode(val) { - return JSON.stringify(utils.isUndefined(val) ? null : val); - } - function decode(val) { - return JSON.parse(val); - } - }(); - var RequestCache = function() { - function RequestCache(o) { - utils.bindAll(this); - o = o || {}; - this.sizeLimit = o.sizeLimit || 10; - this.cache = {}; - this.cachedKeysByAge = []; - } - utils.mixin(RequestCache.prototype, { - get: function(url) { - return this.cache[url]; - }, - set: function(url, resp) { - var requestToEvict; - if (this.cachedKeysByAge.length === this.sizeLimit) { - requestToEvict = this.cachedKeysByAge.shift(); - delete this.cache[requestToEvict]; - } - this.cache[url] = resp; - this.cachedKeysByAge.push(url); - } - }); - return RequestCache; - }(); - var Transport = function() { - var pendingRequestsCount = 0, pendingRequests = {}, maxPendingRequests, requestCache; - function Transport(o) { - utils.bindAll(this); - o = utils.isString(o) ? { - url: o - } : o; - requestCache = requestCache || new RequestCache(); - maxPendingRequests = utils.isNumber(o.maxParallelRequests) ? o.maxParallelRequests : maxPendingRequests || 6; - this.url = o.url; - this.wildcard = o.wildcard || "%QUERY"; - this.filter = o.filter; - this.replace = o.replace; - this.ajaxSettings = { - type: "get", - cache: o.cache, - timeout: o.timeout, - dataType: o.dataType || "json", - beforeSend: o.beforeSend - }; - this._get = (/^throttle$/i.test(o.rateLimitFn) ? utils.throttle : utils.debounce)(this._get, o.rateLimitWait || 300); - } - utils.mixin(Transport.prototype, { - _get: function(url, cb) { - var that = this; - if (belowPendingRequestsThreshold()) { - this._sendRequest(url).done(done); - } else { - this.onDeckRequestArgs = [].slice.call(arguments, 0); - } - function done(resp) { - var data = that.filter ? that.filter(resp) : resp; - cb && cb(data); - requestCache.set(url, resp); - } - }, - _sendRequest: function(url) { - var that = this, jqXhr = pendingRequests[url]; - if (!jqXhr) { - incrementPendingRequests(); - jqXhr = pendingRequests[url] = $.ajax(url, this.ajaxSettings).always(always); - } - return jqXhr; - function always() { - decrementPendingRequests(); - pendingRequests[url] = null; - if (that.onDeckRequestArgs) { - that._get.apply(that, that.onDeckRequestArgs); - that.onDeckRequestArgs = null; - } - } - }, - get: function(query, cb) { - var that = this, encodedQuery = encodeURIComponent(query || ""), url, resp; - cb = cb || utils.noop; - url = this.replace ? this.replace(this.url, encodedQuery) : this.url.replace(this.wildcard, encodedQuery); - if (resp = requestCache.get(url)) { - utils.defer(function() { - cb(that.filter ? that.filter(resp) : resp); - }); - } else { - this._get(url, cb); - } - return !!resp; - } - }); - return Transport; - function incrementPendingRequests() { - pendingRequestsCount++; - } - function decrementPendingRequests() { - pendingRequestsCount--; - } - function belowPendingRequestsThreshold() { - return pendingRequestsCount < maxPendingRequests; - } - }(); - var Dataset = function() { - var keys = { - thumbprint: "thumbprint", - protocol: "protocol", - itemHash: "itemHash", - adjacencyList: "adjacencyList" - }; - function Dataset(o) { - utils.bindAll(this); - if (utils.isString(o.template) && !o.engine) { - $.error("no template engine specified"); - } - if (!o.local && !o.prefetch && !o.remote) { - $.error("one of local, prefetch, or remote is required"); - } - this.name = o.name || utils.getUniqueId(); - this.limit = o.limit || 5; - this.minLength = o.minLength || 1; - this.header = o.header; - this.footer = o.footer; - this.valueKey = o.valueKey || "value"; - this.template = compileTemplate(o.template, o.engine, this.valueKey); - this.local = o.local; - this.prefetch = o.prefetch; - this.remote = o.remote; - this.itemHash = {}; - this.adjacencyList = {}; - this.storage = o.name ? new PersistentStorage(o.name) : null; - } - utils.mixin(Dataset.prototype, { - _processLocalData: function(data) { - this._mergeProcessedData(this._processData(data)); - }, - _loadPrefetchData: function(o) { - var that = this, thumbprint = VERSION + (o.thumbprint || ""), storedThumbprint, storedProtocol, storedItemHash, storedAdjacencyList, isExpired, deferred; - if (this.storage) { - storedThumbprint = this.storage.get(keys.thumbprint); - storedProtocol = this.storage.get(keys.protocol); - storedItemHash = this.storage.get(keys.itemHash); - storedAdjacencyList = this.storage.get(keys.adjacencyList); - } - isExpired = storedThumbprint !== thumbprint || storedProtocol !== utils.getProtocol(); - o = utils.isString(o) ? { - url: o - } : o; - o.ttl = utils.isNumber(o.ttl) ? o.ttl : 24 * 60 * 60 * 1e3; - if (storedItemHash && storedAdjacencyList && !isExpired) { - this._mergeProcessedData({ - itemHash: storedItemHash, - adjacencyList: storedAdjacencyList - }); - deferred = $.Deferred().resolve(); - } else { - deferred = $.getJSON(o.url).done(processPrefetchData); - } - return deferred; - function processPrefetchData(data) { - var filteredData = o.filter ? o.filter(data) : data, processedData = that._processData(filteredData), itemHash = processedData.itemHash, adjacencyList = processedData.adjacencyList; - if (that.storage) { - that.storage.set(keys.itemHash, itemHash, o.ttl); - that.storage.set(keys.adjacencyList, adjacencyList, o.ttl); - that.storage.set(keys.thumbprint, thumbprint, o.ttl); - that.storage.set(keys.protocol, utils.getProtocol(), o.ttl); - } - that._mergeProcessedData(processedData); - } - }, - _transformDatum: function(datum) { - var value = utils.isString(datum) ? datum : datum[this.valueKey], tokens = datum.tokens || utils.tokenizeText(value), item = { - value: value, - tokens: tokens - }; - if (utils.isString(datum)) { - item.datum = {}; - item.datum[this.valueKey] = datum; - } else { - item.datum = datum; - } - item.tokens = utils.filter(item.tokens, function(token) { - return !utils.isBlankString(token); - }); - item.tokens = utils.map(item.tokens, function(token) { - return token.toLowerCase(); - }); - return item; - }, - _processData: function(data) { - var that = this, itemHash = {}, adjacencyList = {}; - utils.each(data, function(i, datum) { - var item = that._transformDatum(datum), id = utils.getUniqueId(item.value); - itemHash[id] = item; - utils.each(item.tokens, function(i, token) { - var character = token.charAt(0), adjacency = adjacencyList[character] || (adjacencyList[character] = [ id ]); - !~utils.indexOf(adjacency, id) && adjacency.push(id); - }); - }); - return { - itemHash: itemHash, - adjacencyList: adjacencyList - }; - }, - _mergeProcessedData: function(processedData) { - var that = this; - utils.mixin(this.itemHash, processedData.itemHash); - utils.each(processedData.adjacencyList, function(character, adjacency) { - var masterAdjacency = that.adjacencyList[character]; - that.adjacencyList[character] = masterAdjacency ? masterAdjacency.concat(adjacency) : adjacency; - }); - }, - _getLocalSuggestions: function(terms) { - var that = this, firstChars = [], lists = [], shortestList, suggestions = []; - utils.each(terms, function(i, term) { - var firstChar = term.charAt(0); - !~utils.indexOf(firstChars, firstChar) && firstChars.push(firstChar); - }); - utils.each(firstChars, function(i, firstChar) { - var list = that.adjacencyList[firstChar]; - if (!list) { - return false; - } - lists.push(list); - if (!shortestList || list.length < shortestList.length) { - shortestList = list; - } - }); - if (lists.length < firstChars.length) { - return []; - } - utils.each(shortestList, function(i, id) { - var item = that.itemHash[id], isCandidate, isMatch; - isCandidate = utils.every(lists, function(list) { - return ~utils.indexOf(list, id); - }); - isMatch = isCandidate && utils.every(terms, function(term) { - return utils.some(item.tokens, function(token) { - return token.indexOf(term) === 0; - }); - }); - isMatch && suggestions.push(item); - }); - return suggestions; - }, - initialize: function() { - var deferred; - this.local && this._processLocalData(this.local); - this.transport = this.remote ? new Transport(this.remote) : null; - deferred = this.prefetch ? this._loadPrefetchData(this.prefetch) : $.Deferred().resolve(); - this.local = this.prefetch = this.remote = null; - this.initialize = function() { - return deferred; - }; - return deferred; - }, - getSuggestions: function(query, cb) { - var that = this, terms, suggestions, cacheHit = false; - if (query.length < this.minLength) { - return; - } - terms = utils.tokenizeQuery(query); - suggestions = this._getLocalSuggestions(terms).slice(0, this.limit); - if (suggestions.length < this.limit && this.transport) { - cacheHit = this.transport.get(query, processRemoteData); - } - !cacheHit && cb && cb(suggestions); - function processRemoteData(data) { - suggestions = suggestions.slice(0); - utils.each(data, function(i, datum) { - var item = that._transformDatum(datum), isDuplicate; - isDuplicate = utils.some(suggestions, function(suggestion) { - //return item.value === suggestion.value; - return false; - }); - !isDuplicate && suggestions.push(item); - return suggestions.length < that.limit; - }); - cb && cb(suggestions); - } - } - }); - return Dataset; - function compileTemplate(template, engine, valueKey) { - var renderFn, compiledTemplate; - if (utils.isFunction(template)) { - renderFn = template; - } else if (utils.isString(template)) { - compiledTemplate = engine.compile(template); - renderFn = utils.bind(compiledTemplate.render, compiledTemplate); - } else { - renderFn = function(context) { - return "

    " + context[valueKey] + "

    "; - }; - } - return renderFn; - } - }(); - var InputView = function() { - function InputView(o) { - var that = this; - utils.bindAll(this); - this.specialKeyCodeMap = { - // START METAMAPS CODE - //9: "tab", - // END METAMAPS CODE - 27: "esc", - 37: "left", - 39: "right", - 13: "enter", - 38: "up", - 40: "down" - }; - this.$hint = $(o.hint); - this.$input = $(o.input).on("blur.tt", this._handleBlur).on("focus.tt", this._handleFocus).on("keydown.tt", this._handleSpecialKeyEvent); - if (!utils.isMsie()) { - this.$input.on("input.tt", this._compareQueryToInputValue); - } else { - this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) { - if (that.specialKeyCodeMap[$e.which || $e.keyCode]) { - return; - } - utils.defer(that._compareQueryToInputValue); - }); - } - this.query = this.$input.val(); - this.$overflowHelper = buildOverflowHelper(this.$input); - } - utils.mixin(InputView.prototype, EventTarget, { - _handleFocus: function() { - this.trigger("focused"); - }, - _handleBlur: function() { - this.trigger("blured"); - }, - _handleSpecialKeyEvent: function($e) { - var keyName = this.specialKeyCodeMap[$e.which || $e.keyCode]; - keyName && this.trigger(keyName + "Keyed", $e); - }, - _compareQueryToInputValue: function() { - var inputValue = this.getInputValue(), isSameQuery = compareQueries(this.query, inputValue), isSameQueryExceptWhitespace = isSameQuery ? this.query.length !== inputValue.length : false; - if (isSameQueryExceptWhitespace) { - this.trigger("whitespaceChanged", { - value: this.query - }); - } else if (!isSameQuery) { - this.trigger("queryChanged", { - value: this.query = inputValue - }); - } - }, - destroy: function() { - this.$hint.off(".tt"); - this.$input.off(".tt"); - this.$hint = this.$input = this.$overflowHelper = null; - }, - focus: function() { - this.$input.focus(); - }, - blur: function() { - this.$input.blur(); - }, - getQuery: function() { - return this.query; - }, - setQuery: function(query) { - this.query = query; - }, - getInputValue: function() { - return this.$input.val(); - }, - setInputValue: function(value, silent) { - this.$input.val(value); - !silent && this._compareQueryToInputValue(); - }, - getHintValue: function() { - return this.$hint.val(); - }, - setHintValue: function(value) { - this.$hint.val(value); - }, - getLanguageDirection: function() { - return (this.$input.css("direction") || "ltr").toLowerCase(); - }, - isOverflow: function() { - this.$overflowHelper.text(this.getInputValue()); - return this.$overflowHelper.width() > this.$input.width(); - }, - isCursorAtEnd: function() { - var valueLength = this.$input.val().length, selectionStart = this.$input[0].selectionStart, range; - if (utils.isNumber(selectionStart)) { - return selectionStart === valueLength; - } else if (document.selection) { - range = document.selection.createRange(); - range.moveStart("character", -valueLength); - return valueLength === range.text.length; - } - return true; - } - }); - return InputView; - function buildOverflowHelper($input) { - return $("").css({ - position: "absolute", - left: "-9999px", - visibility: "hidden", - whiteSpace: "nowrap", - fontFamily: $input.css("font-family"), - fontSize: $input.css("font-size"), - fontStyle: $input.css("font-style"), - fontVariant: $input.css("font-variant"), - fontWeight: $input.css("font-weight"), - wordSpacing: $input.css("word-spacing"), - letterSpacing: $input.css("letter-spacing"), - textIndent: $input.css("text-indent"), - textRendering: $input.css("text-rendering"), - textTransform: $input.css("text-transform") - }).insertAfter($input); - } - function compareQueries(a, b) { - a = (a || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); - b = (b || "").replace(/^\s*/g, "").replace(/\s{2,}/g, " "); - return a === b; - } - }(); - var DropdownView = function() { - var html = { - suggestionsList: '' - }, css = { - suggestionsList: { - display: "block" - }, - suggestion: { - whiteSpace: "nowrap", - cursor: "pointer" - }, - suggestionChild: { - whiteSpace: "normal" - } - }; - function DropdownView(o) { - utils.bindAll(this); - this.isOpen = false; - this.isEmpty = true; - this.isMouseOverDropdown = false; - this.$menu = $(o.menu).on("mouseenter.tt", this._handleMouseenter).on("mouseleave.tt", this._handleMouseleave).on("click.tt", ".tt-suggestion", this._handleSelection).on("mouseover.tt", ".tt-suggestion", this._handleMouseover); - } - utils.mixin(DropdownView.prototype, EventTarget, { - _handleMouseenter: function() { - this.isMouseOverDropdown = true; - }, - _handleMouseleave: function() { - this.isMouseOverDropdown = false; - - // START METAMAPS CODE - this._getSuggestions().removeClass("tt-is-under-cursor"); - this._getSuggestions().removeClass("tt-is-under-mouse-cursor"); - // END METAMAPS CODE - }, - _handleMouseover: function($e) { - var $suggestion = $($e.currentTarget); - this._getSuggestions().removeClass("tt-is-under-cursor"); - // START METAMAPS CODE - this._getSuggestions().removeClass("tt-is-under-mouse-cursor"); - $suggestion.addClass("tt-is-under-mouse-cursor"); - // ORIGINAL CODE $suggestion.addClass("tt-is-under-cursor"); - }, - _handleSelection: function($e) { - var $suggestion = $($e.currentTarget); - this.trigger("suggestionSelected", extractSuggestion($suggestion)); - }, - _show: function() { - this.$menu.css("display", "block"); - }, - _hide: function() { - this.$menu.hide(); - }, - _moveCursor: function(increment) { - var $suggestions, $cur, nextIndex, $underCursor; - if (!this.isVisible()) { - return; - } - $suggestions = this._getSuggestions(); - $cur = $suggestions.filter(".tt-is-under-cursor"); - $cur.removeClass("tt-is-under-cursor"); - nextIndex = $suggestions.index($cur) + increment; - nextIndex = (nextIndex + 1) % ($suggestions.length + 1) - 1; - if (nextIndex === -1) { - this.trigger("cursorRemoved"); - return; - } else if (nextIndex < -1) { - nextIndex = $suggestions.length - 1; - } - $underCursor = $suggestions.eq(nextIndex).addClass("tt-is-under-cursor"); - this._ensureVisibility($underCursor); - this.trigger("cursorMoved", extractSuggestion($underCursor)); - }, - _getSuggestions: function() { - return this.$menu.find(".tt-suggestions > .tt-suggestion"); - }, - _ensureVisibility: function($el) { - var menuHeight = this.$menu.height() + parseInt(this.$menu.css("paddingTop"), 10) + parseInt(this.$menu.css("paddingBottom"), 10), menuScrollTop = this.$menu.scrollTop(), elTop = $el.position().top, elBottom = elTop + $el.outerHeight(true); - if (elTop < 0) { - this.$menu.scrollTop(menuScrollTop + elTop); - } else if (menuHeight < elBottom) { - this.$menu.scrollTop(menuScrollTop + (elBottom - menuHeight)); - } - }, - destroy: function() { - this.$menu.off(".tt"); - this.$menu = null; - }, - isVisible: function() { - return this.isOpen && !this.isEmpty; - }, - closeUnlessMouseIsOverDropdown: function() { - if (!this.isMouseOverDropdown) { - this.close(); - } - }, - close: function() { - if (this.isOpen) { - this.isOpen = false; - this.isMouseOverDropdown = false; - this._hide(); - this.$menu.find(".tt-suggestions > .tt-suggestion").removeClass("tt-is-under-cursor"); - this.trigger("closed"); - } - }, - open: function() { - if (!this.isOpen) { - this.isOpen = true; - !this.isEmpty && this._show(); - this.trigger("opened"); - } - }, - setLanguageDirection: function(dir) { - var ltrCss = { - left: "0", - right: "auto" - }, rtlCss = { - left: "auto", - right: " 0" - }; - dir === "ltr" ? this.$menu.css(ltrCss) : this.$menu.css(rtlCss); - }, - moveCursorUp: function() { - this._moveCursor(-1); - }, - moveCursorDown: function() { - this._moveCursor(+1); - }, - getSuggestionUnderCursor: function() { - var $suggestion = this._getSuggestions().filter(".tt-is-under-cursor").first(); - return $suggestion.length > 0 ? extractSuggestion($suggestion) : null; - }, - getFirstSuggestion: function() { - var $suggestion = this._getSuggestions().first(); - return $suggestion.length > 0 ? extractSuggestion($suggestion) : null; - }, - renderSuggestions: function(dataset, suggestions) { - var datasetClassName = "tt-dataset-" + dataset.name, wrapper = '
    %body
    ', compiledHtml, $suggestionsList, $dataset = this.$menu.find("." + datasetClassName), elBuilder, fragment, $el; - if ($dataset.length === 0) { - $suggestionsList = $(html.suggestionsList).css(css.suggestionsList); - $dataset = $("
    ").addClass(datasetClassName).append(dataset.header).append($suggestionsList).append(dataset.footer).appendTo(this.$menu); - } - if (suggestions.length > 0) { - this.isEmpty = false; - this.isOpen && this._show(); - elBuilder = document.createElement("div"); - fragment = document.createDocumentFragment(); - utils.each(suggestions, function(i, suggestion) { - suggestion.dataset = dataset.name; - compiledHtml = dataset.template(suggestion.datum); - elBuilder.innerHTML = wrapper.replace("%body", compiledHtml); - $el = $(elBuilder.firstChild).css(css.suggestion).data("suggestion", suggestion); - $el.children().each(function() { - $(this).css(css.suggestionChild); - }); - fragment.appendChild($el[0]); - }); - $dataset.show().find(".tt-suggestions").html(fragment); - } else { - this.clearSuggestions(dataset.name); - } - this.trigger("suggestionsRendered"); - }, - clearSuggestions: function(datasetName) { - var $datasets = datasetName ? this.$menu.find(".tt-dataset-" + datasetName) : this.$menu.find('[class^="tt-dataset-"]'), $suggestions = $datasets.find(".tt-suggestions"); - $datasets.hide(); - $suggestions.empty(); - if (this._getSuggestions().length === 0) { - this.isEmpty = true; - this._hide(); - } - } - }); - return DropdownView; - function extractSuggestion($el) { - return $el.data("suggestion"); - } - }(); - var TypeaheadView = function() { - var html = { - wrapper: '', - hint: '', - dropdown: '' - }, css = { - wrapper: { - position: "relative", - display: "inline-block" - }, - hint: { - position: "absolute", - top: "0", - left: "0", - borderColor: "transparent", - boxShadow: "none" - }, - query: { - position: "relative", - verticalAlign: "top", - backgroundColor: "transparent" - }, - dropdown: { - position: "absolute", - top: "100%", - left: "0", - zIndex: "100", - display: "none" - } - }; - if (utils.isMsie()) { - utils.mixin(css.query, { - backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)" - }); - } - if (utils.isMsie() && utils.isMsie() <= 7) { - utils.mixin(css.wrapper, { - display: "inline", - zoom: "1" - }); - utils.mixin(css.query, { - marginTop: "-1px" - }); - } - function TypeaheadView(o) { - var $menu, $input, $hint; - utils.bindAll(this); - this.$node = buildDomStructure(o.input); - this.datasets = o.datasets; - this.dir = null; - this.eventBus = o.eventBus; - $menu = this.$node.find(".tt-dropdown-menu"); - $input = this.$node.find(".tt-query"); - $hint = this.$node.find(".tt-hint"); - this.dropdownView = new DropdownView({ - menu: $menu - }).on("suggestionSelected", this._handleSelection).on("cursorMoved", this._clearHint).on("cursorMoved", this._setInputValueToSuggestionUnderCursor).on("cursorRemoved", this._setInputValueToQuery).on("cursorRemoved", this._updateHint).on("suggestionsRendered", this._updateHint).on("opened", this._updateHint).on("closed", this._clearHint).on("opened closed", this._propagateEvent); - // START METAMAPS CODE - this.dropdownView.on('suggestionsRendered', this._suggestionsRendered); - // END METAMAPS CODE - - this.inputView = new InputView({ - input: $input, - hint: $hint - }).on("focused", this._openDropdown).on("blured", this._closeDropdown).on("blured", this._setInputValueToQuery).on("enterKeyed tabKeyed", this._handleSelection).on("queryChanged", this._clearHint).on("queryChanged", this._clearSuggestions).on("queryChanged", this._getSuggestions).on("whitespaceChanged", this._updateHint).on("queryChanged whitespaceChanged", this._openDropdown).on("queryChanged whitespaceChanged", this._setLanguageDirection).on("escKeyed", this._closeDropdown).on("escKeyed", this._setInputValueToQuery).on("tabKeyed upKeyed downKeyed", this._managePreventDefault).on("upKeyed downKeyed", this._moveDropdownCursor).on("upKeyed downKeyed", this._openDropdown).on("tabKeyed leftKeyed rightKeyed", this._autocomplete); - // START METAMAPS CODE - this.inputView.on('queryChanged', this._queryChanged); - // END METAMAPS CODE - } - utils.mixin(TypeaheadView.prototype, EventTarget, { - _managePreventDefault: function(e) { - var $e = e.data, hint, inputValue, preventDefault = false; - switch (e.type) { - case "tabKeyed": - hint = this.inputView.getHintValue(); - inputValue = this.inputView.getInputValue(); - preventDefault = hint && hint !== inputValue; - break; - - case "upKeyed": - case "downKeyed": - preventDefault = !$e.shiftKey && !$e.ctrlKey && !$e.metaKey; - break; - } - preventDefault && $e.preventDefault(); - }, - _setLanguageDirection: function() { - var dir = this.inputView.getLanguageDirection(); - if (dir !== this.dir) { - this.dir = dir; - this.$node.css("direction", dir); - this.dropdownView.setLanguageDirection(dir); - } - }, - // START METAMAPS CODE - _suggestionsRendered: function() { - this.eventBus.trigger('suggestionsRendered'); - }, - _queryChanged: function() { - this.eventBus.trigger('queryChanged'); - }, - // END METAMAPS CODE - _updateHint: function() { - var suggestion = this.dropdownView.getFirstSuggestion(), hint = suggestion ? suggestion.value : null, dropdownIsVisible = this.dropdownView.isVisible(), inputHasOverflow = this.inputView.isOverflow(), inputValue, query, escapedQuery, beginsWithQuery, match; - if (hint && dropdownIsVisible && !inputHasOverflow) { - inputValue = this.inputView.getInputValue(); - query = inputValue.replace(/\s{2,}/g, " ").replace(/^\s+/g, ""); - escapedQuery = utils.escapeRegExChars(query); - beginsWithQuery = new RegExp("^(?:" + escapedQuery + ")(.*$)", "i"); - match = beginsWithQuery.exec(hint); - this.inputView.setHintValue(inputValue + (match ? match[1] : "")); - } - }, - _clearHint: function() { - this.inputView.setHintValue(""); - }, - _clearSuggestions: function() { - this.dropdownView.clearSuggestions(); - }, - _setInputValueToQuery: function() { - this.inputView.setInputValue(this.inputView.getQuery()); - }, - _setInputValueToSuggestionUnderCursor: function(e) { - var suggestion = e.data; - this.inputView.setInputValue(suggestion.value, true); - }, - _openDropdown: function() { - this.dropdownView.open(); - }, - _closeDropdown: function(e) { - this.dropdownView[e.type === "blured" ? "closeUnlessMouseIsOverDropdown" : "close"](); - }, - _moveDropdownCursor: function(e) { - var $e = e.data; - if (!$e.shiftKey && !$e.ctrlKey && !$e.metaKey) { - this.dropdownView[e.type === "upKeyed" ? "moveCursorUp" : "moveCursorDown"](); - } - }, - _handleSelection: function(e) { - var byClick = e.type === "suggestionSelected", suggestion = byClick ? e.data : this.dropdownView.getSuggestionUnderCursor(); - if (suggestion) { - this.inputView.setInputValue(suggestion.value); - byClick ? this.inputView.focus() : e.data.preventDefault(); - byClick && utils.isMsie() ? utils.defer(this.dropdownView.close) : this.dropdownView.close(); - this.eventBus.trigger("selected", suggestion.datum, suggestion.dataset); - } - }, - _getSuggestions: function() { - var that = this, query = this.inputView.getQuery(); - if (utils.isBlankString(query)) { - return; - } - utils.each(this.datasets, function(i, dataset) { - dataset.getSuggestions(query, function(suggestions) { - if (query === that.inputView.getQuery()) { - that.dropdownView.renderSuggestions(dataset, suggestions); - } - }); - }); - }, - _autocomplete: function(e) { - var isCursorAtEnd, ignoreEvent, query, hint, suggestion; - if (e.type === "rightKeyed" || e.type === "leftKeyed") { - isCursorAtEnd = this.inputView.isCursorAtEnd(); - ignoreEvent = this.inputView.getLanguageDirection() === "ltr" ? e.type === "leftKeyed" : e.type === "rightKeyed"; - if (!isCursorAtEnd || ignoreEvent) { - return; - } - } - query = this.inputView.getQuery(); - hint = this.inputView.getHintValue(); - if (hint !== "" && query !== hint) { - suggestion = this.dropdownView.getFirstSuggestion(); - this.inputView.setInputValue(suggestion.value); - this.eventBus.trigger("autocompleted", suggestion.datum, suggestion.dataset); - } - }, - _propagateEvent: function(e) { - this.eventBus.trigger(e.type); - }, - destroy: function() { - this.inputView.destroy(); - this.dropdownView.destroy(); - destroyDomStructure(this.$node); - this.$node = null; - }, - setQuery: function(query) { - this.inputView.setQuery(query); - this.inputView.setInputValue(query); - this._clearHint(); - this._clearSuggestions(); - this._getSuggestions(); - } - }); - return TypeaheadView; - function buildDomStructure(input) { - var $wrapper = $(html.wrapper), $dropdown = $(html.dropdown), $input = $(input), $hint = $(html.hint); - $wrapper = $wrapper.css(css.wrapper); - $dropdown = $dropdown.css(css.dropdown); - $hint.css(css.hint).css({ - backgroundAttachment: $input.css("background-attachment"), - backgroundClip: $input.css("background-clip"), - backgroundColor: $input.css("background-color"), - backgroundImage: $input.css("background-image"), - backgroundOrigin: $input.css("background-origin"), - backgroundPosition: $input.css("background-position"), - backgroundRepeat: $input.css("background-repeat"), - backgroundSize: $input.css("background-size") - }); - $input.data("ttAttrs", { - dir: $input.attr("dir"), - autocomplete: $input.attr("autocomplete"), - spellcheck: $input.attr("spellcheck"), - style: $input.attr("style") - }); - $input.addClass("tt-query").attr({ - autocomplete: "off", - spellcheck: false - }).css(css.query); - try { - !$input.attr("dir") && $input.attr("dir", "auto"); - } catch (e) {} - return $input.wrap($wrapper).parent().prepend($hint).append($dropdown); - } - function destroyDomStructure($node) { - var $input = $node.find(".tt-query"); - utils.each($input.data("ttAttrs"), function(key, val) { - utils.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val); - }); - $input.detach().removeData("ttAttrs").removeClass("tt-query").insertAfter($node); - $node.remove(); - } - }(); - (function() { - var cache = {}, viewKey = "ttView", methods; - methods = { - initialize: function(datasetDefs) { - var datasets; - datasetDefs = utils.isArray(datasetDefs) ? datasetDefs : [ datasetDefs ]; - if (datasetDefs.length === 0) { - $.error("no datasets provided"); - } - datasets = utils.map(datasetDefs, function(o) { - var dataset = cache[o.name] ? cache[o.name] : new Dataset(o); - if (o.name) { - cache[o.name] = dataset; - } - return dataset; - }); - return this.each(initialize); - function initialize() { - var $input = $(this), deferreds, eventBus = new EventBus({ - el: $input - }); - deferreds = utils.map(datasets, function(dataset) { - return dataset.initialize(); - }); - $input.data(viewKey, new TypeaheadView({ - input: $input, - eventBus: eventBus = new EventBus({ - el: $input - }), - datasets: datasets - })); - $.when.apply($, deferreds).always(function() { - utils.defer(function() { - eventBus.trigger("initialized"); - }); - }); - } - }, - destroy: function() { - return this.each(destroy); - function destroy() { - var $this = $(this), view = $this.data(viewKey); - if (view) { - view.destroy(); - $this.removeData(viewKey); - } - } - }, - setQuery: function(query) { - return this.each(setQuery); - function setQuery() { - var view = $(this).data(viewKey); - view && view.setQuery(query); - } - } - }; - jQuery.fn.typeahead = function(method) { - if (methods[method]) { - return methods[method].apply(this, [].slice.call(arguments, 1)); - } else { - return methods.initialize.apply(this, arguments); - } - }; - })(); -})(window.jQuery); diff --git a/app/assets/javascripts/src/Metamaps.js b/app/assets/javascripts/src/Metamaps.js index af0518d0..842e32b0 100644 --- a/app/assets/javascripts/src/Metamaps.js +++ b/app/assets/javascripts/src/Metamaps.js @@ -672,11 +672,15 @@ Metamaps.Create = { [{ name: 'topic_autocomplete', limit: 8, - template: $('#topicAutocompleteTemplate').html(), - remote: { - url: '/topics/autocomplete_topic?term=%QUERY' + template: Hogan.compile($('#topicAutocompleteTemplate').html()), + source: function(query, syncResults, asyncResults) { + syncResults([]); //we don't got none + var url = '/topics/autocomplete_topic?term=' + query; + $.ajax(url, { + success: function(data) { asyncResults(data); }, + error: function() { asyncResults([]); }, + }); }, - engine: Hogan }] ); @@ -733,23 +737,34 @@ Metamaps.Create = { }, [{ name: 'synapse_autocomplete', - template: "
    {{label}}
    ", - remote: { - url: '/search/synapses?term=%QUERY' + template: Hogan.compile("
    {{label}}
    "), + source: function(query, syncResults, asyncResults) { + syncResults([]); //we don't got none + var url = '/search/synapses?term=' + query; + $.ajax(url, { + success: function(data) { asyncResults(data); }, + error: function() { asyncResults([]); }, + }); }, - engine: Hogan }, { name: 'existing_synapses', limit: 50, - template: $('#synapseAutocompleteTemplate').html(), - remote: { - url: '/search/synapses', - replace: function () { - return self.getSearchQuery(); - } + template: Hogan.compile($('#synapseAutocompleteTemplate').html()), + source: function(query, syncResults, asyncResults) { + syncResults([]); //we don't got none + var self = Metamaps.Create.newSynapse; + + if (Metamaps.Selected.Nodes.length < 2) { + var url = '/search/synapses?topic1id=' + self.topic1id + '&topic2id=' + self.topic2id; + $.ajax(url, { + success: function(data) { asyncResults(data); }, + error: function() { asyncResults([]); }, + }); + } else { + asyncResults([]); + } }, - engine: Hogan, header: "

    Existing synapses

    " }] ); @@ -785,13 +800,6 @@ Metamaps.Create = { Metamaps.Mouse.synapseStartCoordinates = []; Metamaps.Visualize.mGraph.plot(); }, - getSearchQuery: function () { - var self = Metamaps.Create.newSynapse; - - if (Metamaps.Selected.Nodes.length < 2) { - return '/search/synapses?topic1id=' + self.topic1id + '&topic2id=' + self.topic2id; - } else return ''; - } } }; // end Metamaps.Create From a7e512e25adf5a5570b33ba63bc2f31d856a4b6e Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 2 Oct 2015 13:36:51 +0800 Subject: [PATCH 030/305] working version of autocomplete that includes the new typeahead.js syntax. So much more complicated than before... --- app/assets/javascripts/src/Metamaps.js | 92 ++++++++++++++++---------- app/views/layouts/_templates.html.erb | 2 + 2 files changed, 59 insertions(+), 35 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.js b/app/assets/javascripts/src/Metamaps.js index 842e32b0..5f41472d 100644 --- a/app/assets/javascripts/src/Metamaps.js +++ b/app/assets/javascripts/src/Metamaps.js @@ -664,6 +664,15 @@ Metamaps.Create = { Metamaps.Create.newTopic.name = $(this).val(); }); + var topicBloodhound = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/topics/autocomplete_topic?term=%QUERY', + wildcard: '%QUERY', + }, + }); + // initialize the autocomplete results for the metacode spinner $('#topic_name').typeahead( { @@ -672,20 +681,18 @@ Metamaps.Create = { [{ name: 'topic_autocomplete', limit: 8, - template: Hogan.compile($('#topicAutocompleteTemplate').html()), - source: function(query, syncResults, asyncResults) { - syncResults([]); //we don't got none - var url = '/topics/autocomplete_topic?term=' + query; - $.ajax(url, { - success: function(data) { asyncResults(data); }, - error: function() { asyncResults([]); }, - }); + display: function (s) { return s.label; }, + templates: { + suggestion: function(s) { + return Hogan.compile($('#topicAutocompleteTemplate').html()).render(s); + }, }, + source: topicBloodhound, }] ); // tell the autocomplete to submit the form with the topic you clicked on if you pick from the autocomplete - $('#topic_name').bind('typeahead:selected', function (event, datum, dataset) { + $('#topic_name').bind('typeahead:select', function (event, datum, dataset) { Metamaps.Topic.getTopicFromAutocomplete(datum.id); }); @@ -718,7 +725,7 @@ Metamaps.Create = { }, hide: function () { $('#new_topic').fadeOut('fast'); - $("#topic_name").typeahead('setQuery', ''); + $("#topic_name").typeahead('val', ''); Metamaps.Create.newTopic.beingCreated = false; } }, @@ -730,6 +737,31 @@ Metamaps.Create = { Metamaps.Create.newSynapse.description = $(this).val(); }); + var synapseBloodhound = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/synapses?term=%QUERY', + wildcard: '%QUERY', + }, + }); + var existingSynapseBloodhound = new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/synapses?topic1id=%TOPIC1&topic2id=%TOPIC2', + prepare: function(query, settings) { + var self = Metamaps.Create.newSynapse; + if (Metamaps.Selected.Nodes.length < 2) { + var url = '/search/synapses?topic1id=' + self.topic1id + '&topic2id=' + self.topic2id; + } + console.log(query); + console.log(settings); + return settings; + }, + }, + }); + // initialize the autocomplete results for synapse creation $('#synapse_desc').typeahead( { @@ -737,39 +769,29 @@ Metamaps.Create = { }, [{ name: 'synapse_autocomplete', - template: Hogan.compile("
    {{label}}
    "), - source: function(query, syncResults, asyncResults) { - syncResults([]); //we don't got none - var url = '/search/synapses?term=' + query; - $.ajax(url, { - success: function(data) { asyncResults(data); }, - error: function() { asyncResults([]); }, - }); + display: function(s) { return s.label; }, + templates: { + suggestion: function(s) { + return Hogan.compile("
    {{label}}
    ").render(s); + }, }, + source: synapseBloodhound, }, { name: 'existing_synapses', limit: 50, - template: Hogan.compile($('#synapseAutocompleteTemplate').html()), - source: function(query, syncResults, asyncResults) { - syncResults([]); //we don't got none - var self = Metamaps.Create.newSynapse; - - if (Metamaps.Selected.Nodes.length < 2) { - var url = '/search/synapses?topic1id=' + self.topic1id + '&topic2id=' + self.topic2id; - $.ajax(url, { - success: function(data) { asyncResults(data); }, - error: function() { asyncResults([]); }, - }); - } else { - asyncResults([]); - } + display: function(s) { return s.label; }, + templates: { + suggestion: function(s) { + return Hogan.compile($('#synapseAutocompleteTemplate').html()).render(s); + }, + header: "

    Existing synapses

    " }, - header: "

    Existing synapses

    " + source: existingSynapseBloodhound, }] ); - $('#synapse_desc').bind('typeahead:selected', function (event, datum, dataset) { + $('#synapse_desc').bind('typeahead:select', function (event, datum, dataset) { if (datum.id) { // if they clicked on an existing synapse get it Metamaps.Synapse.getSynapseFromAutocomplete(datum.id); } @@ -792,7 +814,7 @@ Metamaps.Create = { }, hide: function () { $('#new_synapse').fadeOut('fast'); - $("#synapse_desc").typeahead('setQuery', ''); + $("#synapse_desc").typeahead('val', ''); Metamaps.Create.newSynapse.beingCreated = false; Metamaps.Create.newTopic.addSynapse = false; Metamaps.Create.newSynapse.topic1id = 0; diff --git a/app/views/layouts/_templates.html.erb b/app/views/layouts/_templates.html.erb index 4762a271..4218bfda 100644 --- a/app/views/layouts/_templates.html.erb +++ b/app/views/layouts/_templates.html.erb @@ -196,6 +196,7 @@ From 675bcadda07cfa93598c81dfd7da31927ddfc834 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 2 Oct 2015 14:39:17 +0800 Subject: [PATCH 031/305] deal with https://github.com/twitter/typeahead.js/issues/1195 --- app/assets/javascripts/src/Metamaps.GlobalUI.js | 4 ++-- app/assets/javascripts/src/Metamaps.js | 4 +--- app/assets/stylesheets/application.css | 16 ++++++++-------- app/assets/stylesheets/clean.css | 6 +++--- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.GlobalUI.js b/app/assets/javascripts/src/Metamaps.GlobalUI.js index 46a34b5c..656c62e5 100644 --- a/app/assets/javascripts/src/Metamaps.GlobalUI.js +++ b/app/assets/javascripts/src/Metamaps.GlobalUI.js @@ -616,7 +616,7 @@ Metamaps.GlobalUI.Search = { var self = Metamaps.GlobalUI.Search; function toggleResultSet(set) { - var s = $('.tt-dataset-' + set + ' .tt-suggestions'); + var s = $('.tt-dataset-' + set + ' .tt-dataset'); if (s.css('height') == '0px') { s.css({ 'height': 'auto', @@ -658,4 +658,4 @@ Metamaps.GlobalUI.Search = { showLoader: function () { $('#searchLoading').show(); } -}; \ No newline at end of file +}; diff --git a/app/assets/javascripts/src/Metamaps.js b/app/assets/javascripts/src/Metamaps.js index 5f41472d..cacdd3c3 100644 --- a/app/assets/javascripts/src/Metamaps.js +++ b/app/assets/javascripts/src/Metamaps.js @@ -37,7 +37,7 @@ Metamaps.Settings = { background: '#18202E', text: '#DDD' } - } + }, }; Metamaps.Touch = { @@ -1564,8 +1564,6 @@ Metamaps.SynapseCard = { ////////////////////// END TOPIC AND SYNAPSE CARDS ////////////////////////////////// - - /* * * VISUALIZE diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 392b2d2c..6ab5085d 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -1373,10 +1373,10 @@ h3.realtimeBoxTitle { /* topic and synapse autocomplete */ -#new_topic .tt-suggestion.tt-is-under-cursor, -#new_topic .tt-suggestion.tt-is-under-mouse-cursor, -#new_synapse .tt-suggestion.tt-is-under-cursor, -#new_synapse .tt-suggestion.tt-is-under-mouse-cursor { +#new_topic .tt-suggestion:hover, +#new_topic .tt-suggestion.tt-cursor, +#new_synapse .tt-suggestion:hover, +#new_synapse .tt-suggestion.tt-cursor { background: #E0E0E0; } #new_topic .tt-suggestion, @@ -1421,12 +1421,12 @@ h3.realtimeBoxTitle { background-image: url(arrowright_sprite.png); background-position: 0 -32px; } -#new_topic .tt-suggestion.tt-is-under-cursor .expandTopicMetadata, -#new_topic .tt-suggestion.tt-is-under-mouse-cursor .expandTopicMetadata { +#new_topic .tt-suggestion:hover .expandTopicMetadata, +#new_topic .tt-suggestion.tt-cursor .expandTopicMetadata { display: block; } -#new_topic .tt-suggestion.tt-is-under-cursor .topicMetadata, -#new_topic .tt-suggestion.tt-is-under-mouse-cursor .topicMetadata { +#new_topic .tt-suggestion:hover .topicMetadata, +#new_topic .tt-suggestion.tt-cursor .topicMetadata { display: block; } #new_topic .topicMetadata { diff --git a/app/assets/stylesheets/clean.css b/app/assets/stylesheets/clean.css index 4ecd3120..d2cc6c7d 100644 --- a/app/assets/stylesheets/clean.css +++ b/app/assets/stylesheets/clean.css @@ -301,7 +301,7 @@ .sidebarSearch .tt-dropdown-menu .maximizeResults { background-position: -32px 0; } -.sidebarSearch .tt-suggestions { +.sidebarSearch .tt-dataset { overflow: visible; } .sidebarSearch .tt-suggestion { @@ -310,7 +310,7 @@ padding: 8px 0; } .sidebarSearch .tt-is-under-cursor, -.sidebarSearch .tt-is-under-mouse-cursor { +.sidebarSearch .tt-suggestion:hover { background: #E0E0E0; } @@ -1227,4 +1227,4 @@ box-shadow: 0px 1px 1.5px rgba(0,0,0,0.12), 0 1px 1px rgba(0,0,0,0.24); body a#barometer_tab:hover { background-position: 0 -110px; -} \ No newline at end of file +} From 379b37b445b0c6b7ffbcbda815acc593f1f85609 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 2 Oct 2015 15:46:48 +0800 Subject: [PATCH 032/305] fix typo + debug statements to make existingSynapse autocomplete work --- app/assets/javascripts/src/Metamaps.js | 8 ++++---- app/controllers/main_controller.rb | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.js b/app/assets/javascripts/src/Metamaps.js index cacdd3c3..e77b9f74 100644 --- a/app/assets/javascripts/src/Metamaps.js +++ b/app/assets/javascripts/src/Metamaps.js @@ -753,11 +753,11 @@ Metamaps.Create = { prepare: function(query, settings) { var self = Metamaps.Create.newSynapse; if (Metamaps.Selected.Nodes.length < 2) { - var url = '/search/synapses?topic1id=' + self.topic1id + '&topic2id=' + self.topic2id; + settings.url = settings.url.replace("%TOPIC1", self.topic1id).replace("%TOPIC2", self.topic2id); + return settings; + } else { + return null; } - console.log(query); - console.log(settings); - return settings; }, }, }); diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 1e8b0c9c..88f035e9 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -219,7 +219,7 @@ class MainController < ApplicationController @synapses = [] end - render json: utocomplete_synapse_array_json(@synapses) + render json: autocomplete_synapse_array_json(@synapses) end end From a4f910b66d117b83ee91eeaedc42ab8b1bf83bee Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 2 Oct 2015 15:53:06 +0800 Subject: [PATCH 033/305] bg color on EXisting synapses heading --- app/assets/stylesheets/application.css | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 6ab5085d..b0a1fa97 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -1380,6 +1380,7 @@ h3.realtimeBoxTitle { background: #E0E0E0; } #new_topic .tt-suggestion, +#new_synapse .tt-dataset h3, #new_synapse .tt-suggestion { background: #F5F5F5; position: relative; From 5dc53543f7bc0f4f3d4ba89e5c0091950022402b Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 2 Oct 2015 16:04:30 +0800 Subject: [PATCH 034/305] migrate to polymorphic mappings - DB MIGRATION --- app/models/map.rb | 4 +- app/models/mapping.rb | 8 +- app/models/synapse.rb | 2 +- app/models/topic.rb | 2 +- .../20151001024122_mapping_polymorphism.rb | 31 ++++++++ db/schema.rb | 75 ++++++++++--------- 6 files changed, 80 insertions(+), 42 deletions(-) create mode 100644 db/migrate/20151001024122_mapping_polymorphism.rb diff --git a/app/models/map.rb b/app/models/map.rb index 6262924e..cccde652 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -4,8 +4,8 @@ class Map < ActiveRecord::Base has_many :topicmappings, -> { Mapping.topicmapping }, class_name: :Mapping, dependent: :destroy has_many :synapsemappings, -> { Mapping.synapsemapping }, class_name: :Mapping, dependent: :destroy - has_many :topics, through: :topicmappings - has_many :synapses, through: :synapsemappings + has_many :topics, through: :topicmappings, source: :mappable, source_type: "Topic" + has_many :synapses, through: :synapsemappings, source: :mappable, source_type: "Synapse" # This method associates the attribute ":image" with a file attachment has_attached_file :screenshot, :styles => { diff --git a/app/models/mapping.rb b/app/models/mapping.rb index a8613840..318aa5cf 100644 --- a/app/models/mapping.rb +++ b/app/models/mapping.rb @@ -1,10 +1,10 @@ class Mapping < ActiveRecord::Base - scope :topicmapping, -> { where(category: :Topic) } - scope :synapsemapping, -> { where(category: :Synapse) } + scope :topicmapping, -> { where(mappable_type: :Topic) } + scope :synapsemapping, -> { where(mappable_type: :Synapse) } + + belongs_to :mappable, polymorphic: true - belongs_to :topic, :class_name => "Topic", :foreign_key => "topic_id" - belongs_to :synapse, :class_name => "Synapse", :foreign_key => "synapse_id" belongs_to :map, :class_name => "Map", :foreign_key => "map_id" belongs_to :user diff --git a/app/models/synapse.rb b/app/models/synapse.rb index 10fba6e9..2711c1c5 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -5,7 +5,7 @@ class Synapse < ActiveRecord::Base belongs_to :topic1, :class_name => "Topic", :foreign_key => "node1_id" belongs_to :topic2, :class_name => "Topic", :foreign_key => "node2_id" - has_many :mappings, dependent: :destroy + has_many :mappings, as: :mappable, dependent: :destroy has_many :maps, :through => :mappings def user_name diff --git a/app/models/topic.rb b/app/models/topic.rb index d28127fa..f82bc256 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -8,7 +8,7 @@ class Topic < ActiveRecord::Base has_many :topics1, :through => :synapses2, :source => :topic1 has_many :topics2, :through => :synapses1, :source => :topic2 - has_many :mappings, dependent: :destroy + has_many :mappings, as: :mappable, dependent: :destroy has_many :maps, :through => :mappings # This method associates the attribute ":image" with a file attachment diff --git a/db/migrate/20151001024122_mapping_polymorphism.rb b/db/migrate/20151001024122_mapping_polymorphism.rb new file mode 100644 index 00000000..6ba88f7c --- /dev/null +++ b/db/migrate/20151001024122_mapping_polymorphism.rb @@ -0,0 +1,31 @@ +class MappingPolymorphism < ActiveRecord::Migration + def up + add_column :mappings, :mappable_id, :integer + add_column :mappings, :mappable_type, :string + add_index :mappings, [:mappable_id, :mappable_type] + + Mapping.find_each do |mapping| + if mapping.synapse_id.nil? and mapping.topic_id.nil? + puts "Mapping id=#{mapping.id} has no valid id, skipping!" + next + end + if not mapping.synapse_id.nil? and not mapping.topic_id.nil? + puts "Mapping id=#{mapping.id} has both topic and synapse ids, skipping!" + next + end + + unless mapping.synapse_id.nil? + mapping.mappable = Synapse.find(mapping.synapse_id) + else + mapping.mappable = Topic.find(mapping.topic_id) + end + mapping.save + end + end + + def down + remove_index :mappings, [:mappable_id, :mappable_type] + remove_column :mappings, :mappable_id, :integer + remove_column :mappings, :mappable_type, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index fc6b7335..ae2c6f03 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -9,21 +9,24 @@ # from scratch. The latter is a flawed and unsustainable approach (the more migrations # you'll amass, the slower it'll run and the greater likelihood for issues). # -# It's strongly recommended to check this file into your version control system. +# It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(:version => 20141121204712) do +ActiveRecord::Schema.define(version: 20151001024122) do - create_table "in_metacode_sets", :force => true do |t| + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + + create_table "in_metacode_sets", force: :cascade do |t| t.integer "metacode_id" t.integer "metacode_set_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - add_index "in_metacode_sets", ["metacode_id"], :name => "index_in_metacode_sets_on_metacode_id" - add_index "in_metacode_sets", ["metacode_set_id"], :name => "index_in_metacode_sets_on_metacode_set_id" + add_index "in_metacode_sets", ["metacode_id"], name: "index_in_metacode_sets_on_metacode_id", using: :btree + add_index "in_metacode_sets", ["metacode_set_id"], name: "index_in_metacode_sets_on_metacode_set_id", using: :btree - create_table "mappings", :force => true do |t| + create_table "mappings", force: :cascade do |t| t.text "category" t.integer "xloc" t.integer "yloc" @@ -31,18 +34,22 @@ ActiveRecord::Schema.define(:version => 20141121204712) do t.integer "synapse_id" t.integer "map_id" t.integer "user_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "mappable_id" + t.string "mappable_type" end - create_table "maps", :force => true do |t| + add_index "mappings", ["mappable_id", "mappable_type"], name: "index_mappings_on_mappable_id_and_mappable_type", using: :btree + + create_table "maps", force: :cascade do |t| t.text "name" t.boolean "arranged" t.text "desc" t.text "permission" t.integer "user_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.boolean "featured" t.string "screenshot_file_name" t.string "screenshot_content_type" @@ -50,26 +57,26 @@ ActiveRecord::Schema.define(:version => 20141121204712) do t.datetime "screenshot_updated_at" end - create_table "metacode_sets", :force => true do |t| + create_table "metacode_sets", force: :cascade do |t| t.string "name" t.text "desc" t.integer "user_id" t.boolean "mapperContributed" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - add_index "metacode_sets", ["user_id"], :name => "index_metacode_sets_on_user_id" + add_index "metacode_sets", ["user_id"], name: "index_metacode_sets_on_user_id", using: :btree - create_table "metacodes", :force => true do |t| + create_table "metacodes", force: :cascade do |t| t.text "name" t.string "icon" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "color" end - create_table "synapses", :force => true do |t| + create_table "synapses", force: :cascade do |t| t.text "desc" t.text "category" t.text "weight" @@ -77,19 +84,19 @@ ActiveRecord::Schema.define(:version => 20141121204712) do t.integer "node1_id" t.integer "node2_id" t.integer "user_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - create_table "topics", :force => true do |t| + create_table "topics", force: :cascade do |t| t.text "name" t.text "desc" t.text "link" t.text "permission" t.integer "user_id" t.integer "metacode_id" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.string "image_file_name" t.string "image_content_type" t.integer "image_file_size" @@ -100,25 +107,25 @@ ActiveRecord::Schema.define(:version => 20141121204712) do t.datetime "audio_updated_at" end - create_table "users", :force => true do |t| + create_table "users", force: :cascade do |t| t.string "name" t.string "email" t.text "settings" - t.string "code", :limit => 8 - t.string "joinedwithcode", :limit => 8 + t.string "code", limit: 8 + t.string "joinedwithcode", limit: 8 t.string "crypted_password" t.string "password_salt" t.string "persistence_token" t.string "perishable_token" - t.datetime "created_at", :null => false - t.datetime "updated_at", :null => false - t.string "encrypted_password", :limit => 128, :default => "" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "encrypted_password", limit: 128, default: "" t.string "remember_token" t.datetime "remember_created_at" t.string "reset_password_token" t.datetime "last_sign_in_at" t.string "last_sign_in_ip" - t.integer "sign_in_count", :default => 0 + t.integer "sign_in_count", default: 0 t.datetime "current_sign_in_at" t.string "current_sign_in_ip" t.datetime "reset_password_sent_at" @@ -130,6 +137,6 @@ ActiveRecord::Schema.define(:version => 20141121204712) do t.integer "generation" end - add_index "users", ["reset_password_token"], :name => "index_users_on_reset_password_token", :unique => true + add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree end From 3e8c971155b2f0cbae05771912de73eca8f73b4d Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 2 Oct 2015 16:28:45 +0800 Subject: [PATCH 035/305] change from category/topic_id/synapse_id to mappable_type/mappable_id --- app/assets/javascripts/src/Metamaps.js | 24 ++++++++++++------------ app/controllers/mappings_controller.rb | 2 +- app/controllers/maps_controller.rb | 14 ++------------ 3 files changed, 15 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.js b/app/assets/javascripts/src/Metamaps.js index e77b9f74..78b6c868 100644 --- a/app/assets/javascripts/src/Metamaps.js +++ b/app/assets/javascripts/src/Metamaps.js @@ -457,11 +457,11 @@ Metamaps.Backbone.init = function () { return Metamaps.Map.get(this.get('map_id')); }, getTopic: function () { - if (this.get('category') === 'Topic') return Metamaps.Topic.get(this.get('topic_id')); + if (this.get('mappable_type') === 'Topic') return Metamaps.Topic.get(this.get('mappable_id')); else return false; }, getSynapse: function () { - if (this.get('category') === 'Synapse') return Metamaps.Synapse.get(this.get('synapse_id')); + if (this.get('mappable_type') === 'Synapse') return Metamaps.Synapse.get(this.get('mappable_id')); else return false; } }); @@ -4109,10 +4109,10 @@ Metamaps.Topic = { Metamaps.Topics.add(topic); var mapping = new Metamaps.Backbone.Mapping({ - category: "Topic", xloc: Metamaps.Create.newTopic.x, yloc: Metamaps.Create.newTopic.y, - topic_id: topic.cid + mappable_id: topic.cid, + mappable_type: "Topic", }); Metamaps.Mappings.add(mapping); @@ -4131,10 +4131,10 @@ Metamaps.Topic = { var topic = self.get(id); var mapping = new Metamaps.Backbone.Mapping({ - category: "Topic", xloc: Metamaps.Create.newTopic.x, yloc: Metamaps.Create.newTopic.y, - topic_id: topic.id + mappable_type: "Topic", + mappable_id: topic.id, }); Metamaps.Mappings.add(mapping); @@ -4149,10 +4149,10 @@ Metamaps.Topic = { var nextCoords = Metamaps.Map.getNextCoord(); var mapping = new Metamaps.Backbone.Mapping({ - category: "Topic", xloc: nextCoords.x, yloc: nextCoords.y, - topic_id: topic.id + mappable_type: "Topic", + mappable_id: topic.id, }); Metamaps.Mappings.add(mapping); @@ -4287,8 +4287,8 @@ Metamaps.Synapse = { Metamaps.Synapses.add(synapse); mapping = new Metamaps.Backbone.Mapping({ - category: "Synapse", - synapse_id: synapse.cid + mappable_type: "Synapse", + mappable_id: synapse.cid, }); Metamaps.Mappings.add(mapping); @@ -4308,8 +4308,8 @@ Metamaps.Synapse = { var synapse = self.get(id); var mapping = new Metamaps.Backbone.Mapping({ - category: "Synapse", - synapse_id: synapse.id + mappable_type: "Synapse", + mappable_id: synapse.id, }); Metamaps.Mappings.add(mapping); diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index 79d8d80a..27567eb4 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -52,6 +52,6 @@ class MappingsController < ApplicationController private # Never trust parameters from the scary internet, only allow the white list through. def mapping_params - params.require(:mapping).permit(:id, :category, :xloc, :yloc, :topic_id, :synapse_id, :map_id, :user_id) + params.require(:mapping).permit(:id, :xloc, :yloc, :mappable_id, :mappable_type, :map_id, :user_id) end end diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 21946f88..7fbddbcc 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -75,11 +75,7 @@ class MapsController < ApplicationController @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } @allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } @allmappings = @map.mappings.to_a.delete_if {|m| - if m.category == "Synapse" - object = m.synapse - elsif m.category == "Topic" - object = m.topic - end + object = m.mappable !object || (object.permission == "private" && (!authenticated? || (authenticated? && @current.id != object.user_id))) } @@ -103,11 +99,7 @@ class MapsController < ApplicationController @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } @allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } @allmappings = @map.mappings.to_a.delete_if {|m| - if m.category == "Synapse" - object = m.synapse - elsif m.category == "Topic" - object = m.topic - end + object = m.mappable !object || (object.permission == "private" && (!authenticated? || (authenticated? && @current.id != object.user_id))) } @@ -141,7 +133,6 @@ class MapsController < ApplicationController @all.each do |topic| topic = topic.split('/') @mapping = Mapping.new() - @mapping.category = "Topic" @mapping.user = @user @mapping.map = @map @mapping.topic = Topic.find(topic[0]) @@ -155,7 +146,6 @@ class MapsController < ApplicationController @synAll = @synAll.split(',') @synAll.each do |synapse_id| @mapping = Mapping.new() - @mapping.category = "Synapse" @mapping.user = @user @mapping.map = @map @mapping.synapse = Synapse.find(synapse_id) From ae16f8f08d5d12235ebf5f98cf11f4726e6cdf90 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 2 Oct 2015 17:39:53 +0800 Subject: [PATCH 036/305] fix a few more mappable/topic/synapse things in JS --- app/assets/javascripts/src/Metamaps.js | 6 ++++-- app/controllers/main_controller.rb | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.js b/app/assets/javascripts/src/Metamaps.js index 78b6c868..5f71007d 100644 --- a/app/assets/javascripts/src/Metamaps.js +++ b/app/assets/javascripts/src/Metamaps.js @@ -194,7 +194,8 @@ Metamaps.Backbone.init = function () { return Metamaps.Mappings.findWhere({ map_id: Metamaps.Active.Map.id, - topic_id: this.isNew() ? this.cid : this.id + mappable_type: "Topic", + mappable_id: this.isNew() ? this.cid : this.id }); }, createNode: function () { @@ -370,7 +371,8 @@ Metamaps.Backbone.init = function () { return Metamaps.Mappings.findWhere({ map_id: Metamaps.Active.Map.id, - synapse_id: this.isNew() ? this.cid : this.id + mappable_type: "Synapse", + mappable_id: this.isNew() ? this.cid : this.id }); }, createEdge: function () { diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 88f035e9..c5535276 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -187,14 +187,14 @@ class MainController < ApplicationController term = params[:term] topic1id = params[:topic1id] topic2id = params[:topic2id] - + if term && !term.empty? - @synapses = Synapse.select('DISTINCT "desc"').where('LOWER("desc") like ?', '%' + term.downcase + '%').order('"desc"') + @synapses = Synapse.where('LOWER("desc") like ?', '%' + term.downcase + '%').order('"desc"') # remove any duplicate synapse types that just differ by # leading or trailing whitespaces collectedDesc = [] - @synapses.to_a.delete_if {|s| + @synapses.to_a.uniq(&:desc).delete_if {|s| desc = s.desc == nil || s.desc == "" ? "" : s.desc.strip if collectedDesc.index(desc) == nil collectedDesc.push(desc) From 840484178b820483512494ad97ce9b1e771790d1 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 21:03:49 +0800 Subject: [PATCH 037/305] fix mapping problem --- app/assets/javascripts/src/Metamaps.js | 40 +++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.js b/app/assets/javascripts/src/Metamaps.js index 5f71007d..a05b6fb7 100644 --- a/app/assets/javascripts/src/Metamaps.js +++ b/app/assets/javascripts/src/Metamaps.js @@ -157,7 +157,7 @@ Metamaps.Backbone.init = function () { this.on('saved', this.savedEvent); this.on('nowPrivate', function(){ var removeTopicData = { - topicid: this.id + mappableid: this.id }; $(document).trigger(Metamaps.JIT.events.removeTopic, [removeTopicData]); @@ -165,7 +165,7 @@ Metamaps.Backbone.init = function () { this.on('noLongerPrivate', function(){ var newTopicData = { mappingid: this.getMapping().id, - topicid: this.id + mappableid: this.id }; $(document).trigger(Metamaps.JIT.events.newTopic, [newTopicData]); @@ -321,14 +321,14 @@ Metamaps.Backbone.init = function () { this.on('noLongerPrivate', function(){ var newSynapseData = { mappingid: this.getMapping().id, - synapseid: this.id + mappableid: this.id }; $(document).trigger(Metamaps.JIT.events.newSynapse, [newSynapseData]); }); this.on('nowPrivate', function(){ $(document).trigger(Metamaps.JIT.events.removeSynapse, [{ - synapseid: this.id + mappableid: this.id }]); }); @@ -2559,7 +2559,7 @@ Metamaps.Realtime = { Metamaps.Mapper.get(data.mapperid, mapperCallback); } $.ajax({ - url: "/topics/" + data.topicid + ".json", + url: "/topics/" + data.mappableid + ".json", success: function (response) { Metamaps.Topics.add(response); topic = Metamaps.Topics.get(response.id); @@ -2606,7 +2606,7 @@ Metamaps.Realtime = { if (!self.status) return; - var topic = Metamaps.Topics.get(data.topicid); + var topic = Metamaps.Topics.get(data.mappableid); if (topic) { var node = topic.get('node'); var mapping = topic.getMapping(); @@ -2657,7 +2657,7 @@ Metamaps.Realtime = { Metamaps.Mapper.get(data.mapperid, mapperCallback); } $.ajax({ - url: "/synapses/" + data.synapseid + ".json", + url: "/synapses/" + data.mappableid + ".json", success: function (response) { Metamaps.Synapses.add(response); synapse = Metamaps.Synapses.get(response.id); @@ -2704,7 +2704,7 @@ Metamaps.Realtime = { if (!self.status) return; - var synapse = Metamaps.Synapses.get(data.synapseid); + var synapse = Metamaps.Synapses.get(data.mappableid); if (synapse) { var edge = synapse.get('edge'); var mapping = synapse.getMapping(); @@ -2814,12 +2814,12 @@ Metamaps.Control = { var permToDelete = Metamaps.Active.Mapper.id === topic.get('user_id') || Metamaps.Active.Mapper.get('admin'); if (permToDelete) { - var topicid = topic.id; + var mappableid = topic.id; var mapping = node.getData('mapping'); topic.destroy(); Metamaps.Mappings.remove(mapping); $(document).trigger(Metamaps.JIT.events.deleteTopic, [{ - topicid: topicid + mappableid: mappableid }]); Metamaps.Control.hideNode(nodeid); } else { @@ -2858,12 +2858,12 @@ Metamaps.Control = { } var topic = node.getData('topic'); - var topicid = topic.id; + var mappableid = topic.id; var mapping = node.getData('mapping'); mapping.destroy(); Metamaps.Topics.remove(topic); $(document).trigger(Metamaps.JIT.events.removeTopic, [{ - topicid: topicid + mappableid: mappableid }]); Metamaps.Control.hideNode(nodeid); }, @@ -2987,7 +2987,7 @@ Metamaps.Control = { Metamaps.Control.hideEdge(edge); } - var synapseid = synapse.id; + var mappableid = synapse.id; synapse.destroy(); // the server will destroy the mapping, we just need to remove it here @@ -2998,7 +2998,7 @@ Metamaps.Control = { delete edge.data.$displayIndex; } $(document).trigger(Metamaps.JIT.events.deleteSynapse, [{ - synapseid: synapseid + mappableid: mappableid }]); } else { Metamaps.GlobalUI.notifyUser('Only synapses you created can be deleted'); @@ -3043,7 +3043,7 @@ Metamaps.Control = { var synapse = edge.getData("synapses")[index]; var mapping = edge.getData("mappings")[index]; - var synapseid = synapse.id; + var mappableid = synapse.id; mapping.destroy(); Metamaps.Synapses.remove(synapse); @@ -3054,7 +3054,7 @@ Metamaps.Control = { delete edge.data.$displayIndex; } $(document).trigger(Metamaps.JIT.events.removeSynapse, [{ - synapseid: synapseid + mappableid: mappableid }]); }, hideSelectedEdges: function () { @@ -4054,14 +4054,14 @@ Metamaps.Topic = { var mappingSuccessCallback = function (mappingModel, response) { var newTopicData = { mappingid: mappingModel.id, - topicid: mappingModel.get('topic_id') + mappableid: mappingModel.get('mappable_id') }; $(document).trigger(Metamaps.JIT.events.newTopic, [newTopicData]); }; var topicSuccessCallback = function (topicModel, response) { if (Metamaps.Active.Map) { - mapping.save({ topic_id: topicModel.id }, { + mapping.save({ mappable_id: topicModel.id }, { success: mappingSuccessCallback, error: function (model, response) { console.log('error saving mapping to database'); @@ -4225,14 +4225,14 @@ Metamaps.Synapse = { var mappingSuccessCallback = function (mappingModel, response) { var newSynapseData = { mappingid: mappingModel.id, - synapseid: mappingModel.get('synapse_id') + mappableid: mappingModel.get('mappable_id') }; $(document).trigger(Metamaps.JIT.events.newSynapse, [newSynapseData]); }; var synapseSuccessCallback = function (synapseModel, response) { if (Metamaps.Active.Map) { - mapping.save({ synapse_id: synapseModel.id }, { + mapping.save({ mappable_id: synapseModel.id }, { success: mappingSuccessCallback }); } From 1453a3c1819b040f5aae9d27c80f71f925aae157 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 21:04:16 +0800 Subject: [PATCH 038/305] function naming --- app/assets/javascripts/src/Metamaps.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.js b/app/assets/javascripts/src/Metamaps.js index a05b6fb7..1e284a6b 100644 --- a/app/assets/javascripts/src/Metamaps.js +++ b/app/assets/javascripts/src/Metamaps.js @@ -2541,7 +2541,7 @@ Metamaps.Realtime = { if (!self.status) return; - function test() { + function waitThenRenderTopic() { if (topic && mapping && mapper) { Metamaps.Topic.renderTopic(mapping, topic, false, false); } @@ -2579,7 +2579,7 @@ Metamaps.Realtime = { } }); - test(); + waitThenRenderTopic(); }, // removeTopic sendDeleteTopic: function (data) { @@ -2634,7 +2634,7 @@ Metamaps.Realtime = { if (!self.status) return; - function test() { + function waitThenRenderSynapse() { if (synapse && mapping && mapper) { topic1 = synapse.getTopic1(); node1 = topic1.get('node'); @@ -2676,7 +2676,7 @@ Metamaps.Realtime = { cancel = true; } }); - test(); + waitThenRenderSynapse(); }, // deleteSynapse sendDeleteSynapse: function (data) { From 617fe43f7143abcee66c53922449f4427a59f22f Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 22:07:44 +0800 Subject: [PATCH 039/305] fix problem with mappings and forking maps --- app/controllers/maps_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 7fbddbcc..add8a0c1 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -135,7 +135,7 @@ class MapsController < ApplicationController @mapping = Mapping.new() @mapping.user = @user @mapping.map = @map - @mapping.topic = Topic.find(topic[0]) + @mapping.mappable = Topic.find(topic[0]) @mapping.xloc = topic[1] @mapping.yloc = topic[2] @mapping.save @@ -148,7 +148,7 @@ class MapsController < ApplicationController @mapping = Mapping.new() @mapping.user = @user @mapping.map = @map - @mapping.synapse = Synapse.find(synapse_id) + @mapping.mappable = Synapse.find(synapse_id) @mapping.save end end From 566a0a3aa60d68da1a99041af7842fb52f319783 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 22:10:45 +0800 Subject: [PATCH 040/305] brute force prevent nil synapse descriptions --- app/assets/javascripts/src/Metamaps.js | 2 +- app/controllers/synapses_controller.rb | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/src/Metamaps.js b/app/assets/javascripts/src/Metamaps.js index 1e284a6b..aea20ab9 100644 --- a/app/assets/javascripts/src/Metamaps.js +++ b/app/assets/javascripts/src/Metamaps.js @@ -4282,7 +4282,7 @@ Metamaps.Synapse = { node1 = synapsesToCreate[i]; topic1 = node1.getData('topic'); synapse = new Metamaps.Backbone.Synapse({ - desc: Metamaps.Create.newSynapse.description, + desc: Metamaps.Create.newSynapse.description || "", node1_id: topic1.isNew() ? topic1.cid : topic1.id, node2_id: topic2.isNew() ? topic2.cid : topic2.id, }); diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index 47d3321e..dc36ff28 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -22,6 +22,7 @@ class SynapsesController < ApplicationController # POST /synapses.json def create @synapse = Synapse.new(synapse_params) + @synapse.update_attribute :desc, "" if @synapse.desc.nil? respond_to do |format| if @synapse.save @@ -36,6 +37,7 @@ class SynapsesController < ApplicationController # PUT /synapses/1.json def update @synapse = Synapse.find(params[:id]) + @synapse.update_attribute :desc, "" if @synapse.desc.nil? respond_to do |format| if @synapse.update_attributes(synapse_params) From 5b882b133201e55f6e7134d72e6d902b62aa6bb8 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 22:27:33 +0800 Subject: [PATCH 041/305] pry byebug --- Gemfile | 1 + Gemfile.lock | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/Gemfile b/Gemfile index 0aa716f8..24fe17fe 100644 --- a/Gemfile +++ b/Gemfile @@ -45,6 +45,7 @@ end group :development, :test do gem 'pry-rails' + gem 'pry-byebug' gem 'better_errors' gem 'quiet_assets' end diff --git a/Gemfile.lock b/Gemfile.lock index 8bd00a57..bff8145f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -52,6 +52,8 @@ GEM erubis (>= 2.6.6) rack (>= 0.9.0) builder (3.2.2) + byebug (4.0.5) + columnize (= 0.9.0) cancancan (1.12.0) climate_control (0.0.3) activesupport (>= 3.0) @@ -65,6 +67,7 @@ GEM coffee-script-source execjs coffee-script-source (1.9.1.1) + columnize (0.9.0) devise (3.5.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -123,6 +126,9 @@ GEM coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) + pry-byebug (3.1.0) + byebug (~> 4.0) + pry (~> 0.10) pry-rails (0.3.4) pry (>= 0.9.10) quiet_assets (1.1.0) @@ -214,6 +220,7 @@ DEPENDENCIES kaminari paperclip pg + pry-byebug pry-rails quiet_assets rails (= 4.2.4) @@ -223,3 +230,6 @@ DEPENDENCIES sass-rails uglifier uservoice-ruby + +BUNDLED WITH + 1.10.6 From 53d77a0e5b79c254719003f4b3850be736d9e04e Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 22:32:09 +0800 Subject: [PATCH 042/305] fix migration for heroku --- db/migrate/20151001024122_mapping_polymorphism.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/db/migrate/20151001024122_mapping_polymorphism.rb b/db/migrate/20151001024122_mapping_polymorphism.rb index 6ba88f7c..e41233f6 100644 --- a/db/migrate/20151001024122_mapping_polymorphism.rb +++ b/db/migrate/20151001024122_mapping_polymorphism.rb @@ -17,6 +17,7 @@ class MappingPolymorphism < ActiveRecord::Migration unless mapping.synapse_id.nil? mapping.mappable = Synapse.find(mapping.synapse_id) else + next if mapping.topic_id == 0 mapping.mappable = Topic.find(mapping.topic_id) end mapping.save From 3f5404b04323a9132d37f37e1d94bd26893fc96a Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 22:38:42 +0800 Subject: [PATCH 043/305] add missing indexes for speed --- db/migrate/20151023143719_add_missing_indexes.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 db/migrate/20151023143719_add_missing_indexes.rb diff --git a/db/migrate/20151023143719_add_missing_indexes.rb b/db/migrate/20151023143719_add_missing_indexes.rb new file mode 100644 index 00000000..551f2319 --- /dev/null +++ b/db/migrate/20151023143719_add_missing_indexes.rb @@ -0,0 +1,16 @@ +class AddMissingIndexes < ActiveRecord::Migration + def change + add_index :topics, :user_id + add_index :topics, :metacode_id + add_index :synapses, [:node2_id, :node2_id] + add_index :synapses, [:node1_id, :node1_id] + add_index :synapses, :user_id + add_index :synapses, :node1_id + add_index :synapses, :node2_id + add_index :mappings, [:map_id, :topic_id] + add_index :mappings, [:map_id, :synapse_id] + add_index :mappings, :map_id + add_index :mappings, :user_id + add_index :maps, :user_id + end +end From 6e9b0ac9ef8e8b6667adf4903b3a8261250771be Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 22:51:16 +0800 Subject: [PATCH 044/305] rename js files to js.erb files --- app/assets/javascripts/src/{JIT.js => JIT.js.erb} | 0 .../src/{Metamaps.Backbone.js => Metamaps.Backbone.js.erb} | 0 .../src/{Metamaps.GlobalUI.js => Metamaps.GlobalUI.js.erb} | 0 .../javascripts/src/{Metamaps.JIT.js => Metamaps.JIT.js.erb} | 0 .../src/{Metamaps.Router.js => Metamaps.Router.js.erb} | 0 .../src/{Metamaps.Views.js => Metamaps.Views.js.erb} | 0 app/assets/javascripts/src/{Metamaps.js => Metamaps.js.erb} | 2 +- 7 files changed, 1 insertion(+), 1 deletion(-) rename app/assets/javascripts/src/{JIT.js => JIT.js.erb} (100%) rename app/assets/javascripts/src/{Metamaps.Backbone.js => Metamaps.Backbone.js.erb} (100%) rename app/assets/javascripts/src/{Metamaps.GlobalUI.js => Metamaps.GlobalUI.js.erb} (100%) rename app/assets/javascripts/src/{Metamaps.JIT.js => Metamaps.JIT.js.erb} (100%) rename app/assets/javascripts/src/{Metamaps.Router.js => Metamaps.Router.js.erb} (100%) rename app/assets/javascripts/src/{Metamaps.Views.js => Metamaps.Views.js.erb} (100%) rename app/assets/javascripts/src/{Metamaps.js => Metamaps.js.erb} (97%) diff --git a/app/assets/javascripts/src/JIT.js b/app/assets/javascripts/src/JIT.js.erb similarity index 100% rename from app/assets/javascripts/src/JIT.js rename to app/assets/javascripts/src/JIT.js.erb diff --git a/app/assets/javascripts/src/Metamaps.Backbone.js b/app/assets/javascripts/src/Metamaps.Backbone.js.erb similarity index 100% rename from app/assets/javascripts/src/Metamaps.Backbone.js rename to app/assets/javascripts/src/Metamaps.Backbone.js.erb diff --git a/app/assets/javascripts/src/Metamaps.GlobalUI.js b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb similarity index 100% rename from app/assets/javascripts/src/Metamaps.GlobalUI.js rename to app/assets/javascripts/src/Metamaps.GlobalUI.js.erb diff --git a/app/assets/javascripts/src/Metamaps.JIT.js b/app/assets/javascripts/src/Metamaps.JIT.js.erb similarity index 100% rename from app/assets/javascripts/src/Metamaps.JIT.js rename to app/assets/javascripts/src/Metamaps.JIT.js.erb diff --git a/app/assets/javascripts/src/Metamaps.Router.js b/app/assets/javascripts/src/Metamaps.Router.js.erb similarity index 100% rename from app/assets/javascripts/src/Metamaps.Router.js rename to app/assets/javascripts/src/Metamaps.Router.js.erb diff --git a/app/assets/javascripts/src/Metamaps.Views.js b/app/assets/javascripts/src/Metamaps.Views.js.erb similarity index 100% rename from app/assets/javascripts/src/Metamaps.Views.js rename to app/assets/javascripts/src/Metamaps.Views.js.erb diff --git a/app/assets/javascripts/src/Metamaps.js b/app/assets/javascripts/src/Metamaps.js.erb similarity index 97% rename from app/assets/javascripts/src/Metamaps.js rename to app/assets/javascripts/src/Metamaps.js.erb index aea20ab9..99377757 100644 --- a/app/assets/javascripts/src/Metamaps.js +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -4282,7 +4282,7 @@ Metamaps.Synapse = { node1 = synapsesToCreate[i]; topic1 = node1.getData('topic'); synapse = new Metamaps.Backbone.Synapse({ - desc: Metamaps.Create.newSynapse.description || "", + desc: Metamaps.Create.newSynapse.description,// || "", node1_id: topic1.isNew() ? topic1.cid : topic1.id, node2_id: topic2.isNew() ? topic2.cid : topic2.id, }); From 235aa7a6b31502e06ea70ec5e132b2b5e28254f4 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 22:56:09 +0800 Subject: [PATCH 045/305] convert javascript to asset_path syntax --- app/assets/javascripts/src/JIT.js.erb | 2 +- app/assets/javascripts/src/Metamaps.GlobalUI.js.erb | 4 ++-- app/assets/javascripts/src/Metamaps.JIT.js.erb | 4 ++-- app/assets/javascripts/src/Metamaps.js.erb | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/src/JIT.js.erb b/app/assets/javascripts/src/JIT.js.erb index 79954271..81cc687d 100644 --- a/app/assets/javascripts/src/JIT.js.erb +++ b/app/assets/javascripts/src/JIT.js.erb @@ -3127,7 +3127,7 @@ var Canvas; ctx = base.getCtx(), scale = base.scaleOffsetX; //var pattern = new Image(); - //pattern.src = "/assets/cubes.png"; + //pattern.src = "<%= asset_path('cubes.png') %>"; //var ptrn = ctx.createPattern(pattern, 'repeat'); //ctx.fillStyle = ptrn; ctx.fillStyle = Metamaps.Settings.colors.background; diff --git a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb index 656c62e5..5c575e88 100644 --- a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb +++ b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb @@ -495,7 +495,7 @@ Metamaps.GlobalUI.Search = { dataset.push({ value: "No results", label: "No results", - typeImageURL: "/assets/icons/wildcard.png", + typeImageURL: "<%= asset_path('icons/wildcard.png') %>", rtype: "noresult" }); } @@ -549,7 +549,7 @@ Metamaps.GlobalUI.Search = { filter: function (dataset) { if (dataset.length == 0) { dataset.push({ - profile: "/assets/user.png", + profile: "<%= asset_path('user.png') %>", value: "No results", label: "No results", diff --git a/app/assets/javascripts/src/Metamaps.JIT.js.erb b/app/assets/javascripts/src/Metamaps.JIT.js.erb index bef4ad59..a0ddf146 100644 --- a/app/assets/javascripts/src/Metamaps.JIT.js.erb +++ b/app/assets/javascripts/src/Metamaps.JIT.js.erb @@ -29,10 +29,10 @@ Metamaps.JIT = { $(".takeScreenshot").click(Metamaps.Map.exportImage); self.topicDescImage = new Image(); - self.topicDescImage.src = '/assets/topic_description_signifier.png'; + self.topicDescImage.src = '<%= asset_path('topic_description_signifier.png') %>'; self.topicLinkImage = new Image(); - self.topicLinkImage.src = '/assets/topic_link_signifier.png'; + self.topicLinkImage.src =<%= asset_path('topic_link_signifier.png') %>'; }, /** * convert our topic JSON into something JIT can use diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index 99377757..9846f4ea 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -337,7 +337,7 @@ Metamaps.Backbone.init = function () { prepareLiForFilter: function () { var li = ''; li += '
  • ';       - li += '"'; li += ' alt="synapse icon" />';       li += '

    ' + this.get('desc') + '

  • '; return li; @@ -4791,7 +4791,7 @@ Metamaps.Map.InfoBox = { obj["map_creator_tip"] = isCreator ? self.changePermissionText : ""; obj["contributors_class"] = Metamaps.Mappers.length > 1 ? "multiple" : ""; obj["contributors_class"] += Metamaps.Mappers.length === 2 ? " mTwo" : ""; - obj["contributor_image"] = Metamaps.Mappers.length > 0 ? Metamaps.Mappers.models[0].get("image") : "/assets/user.png"; + obj["contributor_image"] = Metamaps.Mappers.length > 0 ? Metamaps.Mappers.models[0].get("image") : "<%= asset_path('user.png') %>"; obj["contributor_list"] = self.createContributorList(); obj["user_name"] = isCreator ? "You" : map.get("user_name"); obj["created_at"] = map.get("created_at_clean"); @@ -4888,7 +4888,7 @@ Metamaps.Map.InfoBox = { if (Metamaps.Mappers.length === 2) contributors_class = "multiple mTwo"; else if (Metamaps.Mappers.length > 2) contributors_class = "multiple"; - var contributors_image = "/assets/user.png"; + var contributors_image = "<%= asset_path('user.png') %>"; if (Metamaps.Mappers.length > 0) { // get the first contributor and use their image contributors_image = Metamaps.Mappers.models[0].get("image"); @@ -5069,7 +5069,7 @@ Metamaps.Account = { var self = Metamaps.Account; $('.userImageDiv canvas').remove(); - $('.userImageDiv img').attr('src', '/assets/user.png').show(); + $('.userImageDiv img').attr('src', '<%= asset_path('user.png') %>').show(); $('.userImageMenu').hide(); var input = $('#user_image'); From 2505ce36a57ae9a1d744b939574fb35f6ccb8224 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 23:01:02 +0800 Subject: [PATCH 046/305] syntax --- app/assets/javascripts/src/Metamaps.JIT.js.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/src/Metamaps.JIT.js.erb b/app/assets/javascripts/src/Metamaps.JIT.js.erb index a0ddf146..38a2de5b 100644 --- a/app/assets/javascripts/src/Metamaps.JIT.js.erb +++ b/app/assets/javascripts/src/Metamaps.JIT.js.erb @@ -32,7 +32,7 @@ Metamaps.JIT = { self.topicDescImage.src = '<%= asset_path('topic_description_signifier.png') %>'; self.topicLinkImage = new Image(); - self.topicLinkImage.src =<%= asset_path('topic_link_signifier.png') %>'; + self.topicLinkImage.src = '<%= asset_path('topic_link_signifier.png') %>'; }, /** * convert our topic JSON into something JIT can use From cef83e1f3d76707c8cf7642987b7f5e463febb2e Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 23:06:24 +0800 Subject: [PATCH 047/305] asset_path --- app/helpers/maps_helper.rb | 2 +- app/models/map.rb | 2 +- app/models/user.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/maps_helper.rb b/app/helpers/maps_helper.rb index 0c9b3a08..6b3dd7b0 100644 --- a/app/helpers/maps_helper.rb +++ b/app/helpers/maps_helper.rb @@ -16,7 +16,7 @@ module MapsHelper map['rtype'] = "map" contributorTip = '' - firstContributorImage = '/assets/user.png' + firstContributorImage = asset_path('user.png') if m.contributors.count > 0 firstContributorImage = m.contributors[0].image.url(:thirtytwo) m.contributors.each_with_index do |c, index| diff --git a/app/models/map.rb b/app/models/map.rb index cccde652..6d2a3332 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -12,7 +12,7 @@ class Map < ActiveRecord::Base :thumb => ['188x126#', :png] #:full => ['940x630#', :png] }, - :default_url => "/assets/missing-map.png" + :default_url => asset_path('missing-map.png') # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :screenshot, :content_type => /\Aimage\/.*\Z/ diff --git a/app/models/user.rb b/app/models/user.rb index 44bd6b4a..eb69829d 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,7 +35,7 @@ class User < ActiveRecord::Base :ninetysix => ['96x96#', :png], :onetwentyeight => ['128x128#', :png] }, - :default_url => "/assets/user.png" + :default_url => asset_path('user.png') # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :image, :content_type => /\Aimage\/.*\Z/ From b9ac6140571376cff0181df6970592e401967734 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 23:09:14 +0800 Subject: [PATCH 048/305] asset_path doesn't work in models --- app/models/map.rb | 2 +- app/models/user.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/map.rb b/app/models/map.rb index 6d2a3332..eab88814 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -12,7 +12,7 @@ class Map < ActiveRecord::Base :thumb => ['188x126#', :png] #:full => ['940x630#', :png] }, - :default_url => asset_path('missing-map.png') + :default_url => ActionController::Base.helpers.asset_path('missing-map.png') # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :screenshot, :content_type => /\Aimage\/.*\Z/ diff --git a/app/models/user.rb b/app/models/user.rb index eb69829d..effa6ec5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,7 +35,7 @@ class User < ActiveRecord::Base :ninetysix => ['96x96#', :png], :onetwentyeight => ['128x128#', :png] }, - :default_url => asset_path('user.png') + :default_url => ActionController::Base.helpers.asset_path('user.png') # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :image, :content_type => /\Aimage\/.*\Z/ From 4d7f5092358eb04b7869a77897200b685ef836c0 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 23:19:25 +0800 Subject: [PATCH 049/305] change some css to erb --- app/assets/stylesheets/{application.css => application.css.erb} | 2 +- app/assets/stylesheets/{clean.css => clean.css.erb} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/assets/stylesheets/{application.css => application.css.erb} (95%) rename app/assets/stylesheets/{clean.css => clean.css.erb} (99%) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css.erb similarity index 95% rename from app/assets/stylesheets/application.css rename to app/assets/stylesheets/application.css.erb index b0a1fa97..dfb5aa78 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css.erb @@ -813,7 +813,7 @@ li.accountInvite span { padding: 9px 0 9px 62px; } .accountImage { - background-image: url(user.png); + background-image: url(<%= asset_data_uri 'user.png' %>); background-size: 84px 84px; background-repeat: no-repeat; height:84px; diff --git a/app/assets/stylesheets/clean.css b/app/assets/stylesheets/clean.css.erb similarity index 99% rename from app/assets/stylesheets/clean.css rename to app/assets/stylesheets/clean.css.erb index d2cc6c7d..793e328c 100644 --- a/app/assets/stylesheets/clean.css +++ b/app/assets/stylesheets/clean.css.erb @@ -918,7 +918,7 @@ .takeScreenshot { margin-bottom: 5px; border-radius: 2px; - background-image: url(screenshot_sprite.png); + background-image: url(<%= asset_data_uri 'screenshot_sprite.png' %>); display: none; } .takeScreenshot:hover { From d52caac1d6411a6ea609ba44dbc0dbe58777f962 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 23:22:54 +0800 Subject: [PATCH 050/305] add indexes to schema --- db/schema.rb | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/db/schema.rb b/db/schema.rb index ae2c6f03..97bf204a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151001024122) do +ActiveRecord::Schema.define(version: 20151023143719) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -40,7 +40,11 @@ ActiveRecord::Schema.define(version: 20151001024122) do t.string "mappable_type" end + add_index "mappings", ["map_id", "synapse_id"], name: "index_mappings_on_map_id_and_synapse_id", using: :btree + add_index "mappings", ["map_id", "topic_id"], name: "index_mappings_on_map_id_and_topic_id", using: :btree + add_index "mappings", ["map_id"], name: "index_mappings_on_map_id", using: :btree add_index "mappings", ["mappable_id", "mappable_type"], name: "index_mappings_on_mappable_id_and_mappable_type", using: :btree + add_index "mappings", ["user_id"], name: "index_mappings_on_user_id", using: :btree create_table "maps", force: :cascade do |t| t.text "name" @@ -57,6 +61,8 @@ ActiveRecord::Schema.define(version: 20151001024122) do t.datetime "screenshot_updated_at" end + add_index "maps", ["user_id"], name: "index_maps_on_user_id", using: :btree + create_table "metacode_sets", force: :cascade do |t| t.string "name" t.text "desc" @@ -88,6 +94,12 @@ ActiveRecord::Schema.define(version: 20151001024122) do t.datetime "updated_at", null: false end + add_index "synapses", ["node1_id", "node1_id"], name: "index_synapses_on_node1_id_and_node1_id", using: :btree + add_index "synapses", ["node1_id"], name: "index_synapses_on_node1_id", using: :btree + add_index "synapses", ["node2_id", "node2_id"], name: "index_synapses_on_node2_id_and_node2_id", using: :btree + add_index "synapses", ["node2_id"], name: "index_synapses_on_node2_id", using: :btree + add_index "synapses", ["user_id"], name: "index_synapses_on_user_id", using: :btree + create_table "topics", force: :cascade do |t| t.text "name" t.text "desc" @@ -107,6 +119,9 @@ ActiveRecord::Schema.define(version: 20151001024122) do t.datetime "audio_updated_at" end + add_index "topics", ["metacode_id"], name: "index_topics_on_metacode_id", using: :btree + add_index "topics", ["user_id"], name: "index_topics_on_user_id", using: :btree + create_table "users", force: :cascade do |t| t.string "name" t.string "email" From 6a21b84a2343bb079bfb2239b6811dcdac340f6f Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 23:23:23 +0800 Subject: [PATCH 051/305] resume ignoring public/assets --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 75aa3c26..2b6ac398 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ realtime/node_modules config/database.yml .env -#public/assets +public/assets # Ignore bundler config .bundle From 52b37e7ac6f856689afff079cbd8c436354e0fc6 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 23:34:18 +0800 Subject: [PATCH 052/305] asset_path in css erb files --- app/assets/stylesheets/application.css.erb | 6 +++--- .../stylesheets/{base.css => base.css.erb} | 4 ++-- app/assets/stylesheets/clean.css.erb | 20 +++++++++---------- app/controllers/synapses_controller.rb | 1 + app/models/synapse.rb | 2 ++ 5 files changed, 18 insertions(+), 15 deletions(-) rename app/assets/stylesheets/{base.css => base.css.erb} (94%) diff --git a/app/assets/stylesheets/application.css.erb b/app/assets/stylesheets/application.css.erb index dfb5aa78..ed4ec2ac 100644 --- a/app/assets/stylesheets/application.css.erb +++ b/app/assets/stylesheets/application.css.erb @@ -2352,15 +2352,15 @@ and it won't be important on password protected instances */ opacity: 0.7; } #chromeIcon { - background: url(/assets/browser_icons.png) no-repeat; + background: url(<%= asset_data_uri 'browser_icons.png' %>) no-repeat; } #fireFoxIcon { - background: url(/assets/browser_icons.png) no-repeat -105px 0; + background: url(<%= asset_data_uri 'browser_icons.png' %>) no-repeat -105px 0; } #safariIcon { - background: url(/assets/browser_icons.png) no-repeat -220px 0; + background: url(<%= asset_data_uri 'browser_icons.png' %>) no-repeat -220px 0; } #chromeIcon:hover{ diff --git a/app/assets/stylesheets/base.css b/app/assets/stylesheets/base.css.erb similarity index 94% rename from app/assets/stylesheets/base.css rename to app/assets/stylesheets/base.css.erb index 2e0073a0..858163ad 100644 --- a/app/assets/stylesheets/base.css +++ b/app/assets/stylesheets/base.css.erb @@ -740,7 +740,7 @@ font-family: 'din-regular', helvetica, sans-serif; } #linkremove { - background-image: url(/assets/remove.png); + background-image: url(<%= asset_data_uri 'remove.png' %>); background-repeat: no-repeat; background-position: center center; width: 24px; @@ -1126,4 +1126,4 @@ font-family: 'din-regular', helvetica, sans-serif; } .mapperMetadata .metadataSynapses { color: #DAB539; -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index 793e328c..456c6afc 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -1,20 +1,20 @@ @font-face { font-family: 'din-medium'; - src: url('/assets/Fonts/din.eot'); - src: url('/assets/Fonts/din.eot?#iefix') format('embedded-opentype'), - url('/assets/Fonts/din.woff') format('woff'), - url('/assets/Fonts/din.ttf') format('truetype'), - url('/assets/Fonts/din.svg#din-medium') format('svg'); + src: url(<%= asset_path 'Fonts/din.eot' %>); + src: url(<%= asset_path 'Fonts/din.eot?#iefix' %>) format('embedded-opentype'), + url(<%= asset_path 'Fonts/din.woff' %>) format('woff'), + url(<%= asset_path 'Fonts/din.ttf' %>) format('truetype'), + url(<%= asset_path 'Fonts/din.svg#din-medium' %>) format('svg'); font-weight: normal; font-style: normal; } @font-face { font-family: 'din-regular'; - src: url('/assets/Fonts/din-reg.eot'); - src: url('/assets/Fonts/din-reg.eot?#iefix') format('embedded-opentype'), - url('/assets/Fonts/din-reg.woff') format('woff'), - url('/assets/Fonts/din-reg.ttf') format('truetype'), - url('/assets/Fonts/din-reg.svg#din-reg') format('svg'); + src: url(<%= asset_path 'Fonts/din-reg.eot' %>); + src: url(<%= asset_path 'Fonts/din-reg.eot?#iefix' %>) format('embedded-opentype'), + url(<%= asset_path 'Fonts/din-reg.woff' %>) format('woff'), + url(<%= asset_path 'Fonts/din-reg.ttf' %>) format('truetype'), + url(<%= asset_path 'Fonts/din-reg.svg#din-reg' %>) format('svg'); font-weight: normal; font-style: normal; } diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index dc36ff28..db30a058 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -21,6 +21,7 @@ class SynapsesController < ApplicationController # POST /synapses # POST /synapses.json def create + binding.pry @synapse = Synapse.new(synapse_params) @synapse.update_attribute :desc, "" if @synapse.desc.nil? diff --git a/app/models/synapse.rb b/app/models/synapse.rb index 2711c1c5..2745106f 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -8,6 +8,8 @@ class Synapse < ActiveRecord::Base has_many :mappings, as: :mappable, dependent: :destroy has_many :maps, :through => :mappings + validates :desc, length: { minimum: 0, allow_nil: false } + def user_name self.user.name end From c782c4d0bb446af475ab7155d97ef36147e73014 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 23:42:21 +0800 Subject: [PATCH 053/305] remove binding.pry --- app/controllers/synapses_controller.rb | 1 - app/views/maps/_newtopic.html.erb | 2 +- app/views/metacode_sets/_form.html.erb | 8 ++++---- app/views/metacode_sets/index.html.erb | 2 +- app/views/metacodes/index.html.erb | 4 ++-- app/views/metacodes/new.html.erb | 2 +- app/views/shared/_cheatsheet.html.erb | 2 +- app/views/shared/_metacodeoptions.html.erb | 4 ++-- app/views/shared/_switchmetacodes.html.erb | 4 ++-- app/views/topics/_new.html.erb | 2 +- 10 files changed, 15 insertions(+), 16 deletions(-) diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index db30a058..dc36ff28 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -21,7 +21,6 @@ class SynapsesController < ApplicationController # POST /synapses # POST /synapses.json def create - binding.pry @synapse = Synapse.new(synapse_params) @synapse.update_attribute :desc, "" if @synapse.desc.nil? diff --git a/app/views/maps/_newtopic.html.erb b/app/views/maps/_newtopic.html.erb index 16b5fecb..e5263d76 100644 --- a/app/views/maps/_newtopic.html.erb +++ b/app/views/maps/_newtopic.html.erb @@ -4,7 +4,7 @@ <% @metacodes = user_metacodes() %> <% set = get_metacodeset() %> <% @metacodes.each do |metacode| %> - <%= metacode.name %> + <%= metacode.name %> <% end %>
    <%= form.text_field :name, :maxlength => 140, :placeholder => "title..." %> diff --git a/app/views/metacode_sets/_form.html.erb b/app/views/metacode_sets/_form.html.erb index e3d1ae40..f7ef60c6 100644 --- a/app/views/metacode_sets/_form.html.erb +++ b/app/views/metacode_sets/_form.html.erb @@ -37,7 +37,7 @@ <% while $i < (Metacode.all.length / 4) do %>
  • class="toggledOff"<% end %> onclick="Metamaps.Admin.liClickHandler.call(this);"> - <%= @m[$i].name %> + <%= @m[$i].name %>

    <%= @m[$i].name.downcase %>

  • @@ -48,7 +48,7 @@ <% while $i < (Metacode.all.length / 4 * 2) do %>
  • class="toggledOff"<% end %> onclick="Metamaps.Admin.liClickHandler.call(this);"> - <%= @m[$i].name %> + <%= @m[$i].name %>

    <%= @m[$i].name.downcase %>

  • @@ -59,7 +59,7 @@ <% while $i < (Metacode.all.length / 4 * 3) do %>
  • class="toggledOff"<% end %> onclick="Metamaps.Admin.liClickHandler.call(this);"> - <%= @m[$i].name %> + <%= @m[$i].name %>

    <%= @m[$i].name.downcase %>

  • @@ -70,7 +70,7 @@ <% while $i < Metacode.all.length do %>
  • class="toggledOff"<% end %> onclick="Metamaps.Admin.liClickHandler.call(this);"> - <%= @m[$i].name %> + <%= @m[$i].name %>

    <%= @m[$i].name.downcase %>

  • diff --git a/app/views/metacode_sets/index.html.erb b/app/views/metacode_sets/index.html.erb index ba83b0a3..8f6aa810 100644 --- a/app/views/metacode_sets/index.html.erb +++ b/app/views/metacode_sets/index.html.erb @@ -23,7 +23,7 @@ <%= metacode_set.desc %> <% metacode_set.metacodes.each_with_index do |metacode, index| %> - + <% if (index+1)%4 == 0 %>
    <% end %> diff --git a/app/views/metacodes/index.html.erb b/app/views/metacodes/index.html.erb index 1ebdb5da..4f3563f1 100644 --- a/app/views/metacodes/index.html.erb +++ b/app/views/metacodes/index.html.erb @@ -14,7 +14,7 @@ <% @metacodes.each do |metacode| %> <%= metacode.name %> - <%= metacode.icon %> + <%= asset_path metacode.icon %> <% if metacode.color %> <%= metacode.color %> @@ -22,7 +22,7 @@ <% else %> <% end %> - + <%= link_to 'Edit', edit_metacode_path(metacode), :data => { :bypass => 'true'} %> <% end %> diff --git a/app/views/metacodes/new.html.erb b/app/views/metacodes/new.html.erb index 8520bb6c..e10f28d1 100644 --- a/app/views/metacodes/new.html.erb +++ b/app/views/metacodes/new.html.erb @@ -2,4 +2,4 @@
    <%= render 'form' %>
    -
    \ No newline at end of file +
    diff --git a/app/views/shared/_cheatsheet.html.erb b/app/views/shared/_cheatsheet.html.erb index 0af70ec1..7332a1c7 100644 --- a/app/views/shared/_cheatsheet.html.erb +++ b/app/views/shared/_cheatsheet.html.erb @@ -61,7 +61,7 @@ Change Topic permission: Click on 'Permission' icon (only for topic creator)
    - Open Topic view: Click on icon within topic card bar + Open Topic view: Click on icon within topic card bar
    Close 'Topic' card: Click on canvas diff --git a/app/views/shared/_metacodeoptions.html.erb b/app/views/shared/_metacodeoptions.html.erb index 25aae79e..a6092c3e 100644 --- a/app/views/shared/_metacodeoptions.html.erb +++ b/app/views/shared/_metacodeoptions.html.erb @@ -12,7 +12,7 @@
      <% set.metacodes.sort { |a, b| a.name <=> b.name }.each do |m| %>
    • - <%= m.name %> + <%= m.name %>
      <%= m.name %>
    • @@ -26,7 +26,7 @@
        <% Metacode.order("name").all.each do |m| %>
      • - <%= m.name %> + <%= m.name %>
        <%= m.name %>
      • diff --git a/app/views/shared/_switchmetacodes.html.erb b/app/views/shared/_switchmetacodes.html.erb index 3d683198..067d4d6b 100644 --- a/app/views/shared/_switchmetacodes.html.erb +++ b/app/views/shared/_switchmetacodes.html.erb @@ -28,7 +28,7 @@ data-metacodes="<%= m.metacodes.map(&:id).join(',') %>"> <% @list = '' %> <% m.metacodes.sort{|x,y| x.name <=> y.name }.each_with_index do |m, index| %> - <% @list += '
      • ' + m.name + '

        ' + m.name.downcase + '

      • ' %> + <% @list += '
      • ' + m.name + '

        ' + m.name.downcase + '

      • ' %> <% end %>

        <%= m.desc %>

        @@ -53,7 +53,7 @@ <% else %> <% mClass = "toggledOff" %> <% end %> - <% @list += '
      • ' + m.name + '

        ' + m.name.downcase + '

      • ' %> + <% @list += '
      • ' + m.name + '

        ' + m.name.downcase + '

      • ' %> <% end %>
        diff --git a/app/views/topics/_new.html.erb b/app/views/topics/_new.html.erb index 7aa13eb3..49b6a9a8 100644 --- a/app/views/topics/_new.html.erb +++ b/app/views/topics/_new.html.erb @@ -20,7 +20,7 @@ <% end %> <% @metacodes.sort! {|m1,m2| m2.name.downcase <=> m1.name.downcase }.rotate!(-1) %> <% @metacodes.each do |metacode| %> - <%= metacode.name %> + <%= metacode.name %> <% end %>
        <%= form.text_field :name, :maxlength => 140, :placeholder => "title..." %> From 29411cf1e5a8926c8b56aea7408e47288082932c Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 23 Oct 2015 23:56:29 +0800 Subject: [PATCH 054/305] asset_path --- app/assets/stylesheets/clean.css.erb | 8 ++++---- .../stylesheets/{uservoice.css => uservoice.css.erb} | 4 ++-- app/views/maps/_mapinfobox.html.erb | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) rename app/assets/stylesheets/{uservoice.css => uservoice.css.erb} (85%) diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index 456c6afc..008e8c9d 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -1163,15 +1163,15 @@ left:5px; } .exploreMapsCenter .myMaps .exploreMapsIcon { - background-image: url(exploremaps_sprite.png); + background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: 0 0; } .exploreMapsCenter .activeMaps .exploreMapsIcon { - background-image: url(exploremaps_sprite.png); + background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: -32px 0; } .exploreMapsCenter .featuredMaps .exploreMapsIcon { - background-image: url(exploremaps_sprite.png); + background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: -64px 0; } .myMaps:hover .exploreMapsIcon, .myMaps.active .exploreMapsIcon { @@ -1212,7 +1212,7 @@ /* feedback */ body a#barometer_tab { -background-image: url(feedback_sprite.png); +background-image: url(<%= asset_path 'feedback_sprite.png' %>); background-position: 0 0; background-color: transparent; height: 110px; diff --git a/app/assets/stylesheets/uservoice.css b/app/assets/stylesheets/uservoice.css.erb similarity index 85% rename from app/assets/stylesheets/uservoice.css rename to app/assets/stylesheets/uservoice.css.erb index 02b92b9e..df15d675 100644 --- a/app/assets/stylesheets/uservoice.css +++ b/app/assets/stylesheets/uservoice.css.erb @@ -6,7 +6,7 @@ } div.uv-icon.uv-bottom-left { - background-image:url(feedback_sprite.png); + background-image:url(<%= asset_data_uri 'feedback_sprite.png' %>); background-repeat: no-repeat; color:#FFFFFF; cursor:pointer; @@ -22,4 +22,4 @@ div.uv-icon.uv-bottom-left { div.uv-icon.uv-bottom-left:hover { background-position: 0 -110px; -} \ No newline at end of file +} diff --git a/app/views/maps/_mapinfobox.html.erb b/app/views/maps/_mapinfobox.html.erb index 43381835..1fa6da02 100644 --- a/app/views/maps/_mapinfobox.html.erb +++ b/app/views/maps/_mapinfobox.html.erb @@ -23,7 +23,7 @@ <% end %> <%= @map.contributors.count %>
        From d3080906b1bdf7a7592c50636f6489281af7da5e Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 25 Oct 2015 16:37:13 +0800 Subject: [PATCH 055/305] add db migration to remove asset paths from metacodes --- .../20151025083043_remove_asset_paths_from_metacodes.rb | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 db/migrate/20151025083043_remove_asset_paths_from_metacodes.rb diff --git a/db/migrate/20151025083043_remove_asset_paths_from_metacodes.rb b/db/migrate/20151025083043_remove_asset_paths_from_metacodes.rb new file mode 100644 index 00000000..c5a5e842 --- /dev/null +++ b/db/migrate/20151025083043_remove_asset_paths_from_metacodes.rb @@ -0,0 +1,8 @@ +class RemoveAssetPathsFromMetacodes < ActiveRecord::Migration + def change + Metacode.all.each do |metacode| + metacode.icon = metacode.icon.gsub(/^\/assets\//, '') + metacode.save + end + end +end From 96f5e6ac3562441fe68c3a67392a367150bf68a1 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 25 Oct 2015 16:50:07 +0800 Subject: [PATCH 056/305] add asset_path calls --- app/helpers/topics_helper.rb | 2 +- app/views/shared/_filterBox.html.erb | 2 +- db/schema.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index 482a663c..79945b6f 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -10,7 +10,7 @@ module TopicsHelper topic['value'] = t.name topic['description'] = t.desc.truncate(70) # make this return matched results topic['type'] = t.metacode.name - topic['typeImageURL'] = t.metacode.icon + topic['typeImageURL'] = asset_path(t.metacode.icon) topic['permission'] = t.permission topic['mapCount'] = t.maps.count topic['synapseCount'] = t.synapses.count diff --git a/app/views/shared/_filterBox.html.erb b/app/views/shared/_filterBox.html.erb index 714b1378..9ea4926b 100644 --- a/app/views/shared/_filterBox.html.erb +++ b/app/views/shared/_filterBox.html.erb @@ -70,7 +70,7 @@ @metacodes.each_with_index do |metacode, index| @metacodelist += '
      • ' - @metacodelist += '' + metacode.name + '' + @metacodelist += '' + metacode.name + '' @metacodelist += '

        ' + metacode.name.downcase + '

      • ' end @synapses.each_with_index do |synapse, index| diff --git a/db/schema.rb b/db/schema.rb index 97bf204a..45fbcf67 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20151023143719) do +ActiveRecord::Schema.define(version: 20151025083043) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" From bd60f68cfe905a6dc231834e5a739a0564da1887 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 25 Oct 2015 16:51:47 +0800 Subject: [PATCH 057/305] try to compile but not compress assets --- config/environments/development.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index 53e8b4fb..c4d3540e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -40,7 +40,8 @@ Metamaps::Application.configure do # Print deprecation notices to the Rails logger config.active_support.deprecation = :log - # Do not compress assets + # Compress assets + config.assets.compile = true config.assets.compress = false # Expands the lines which load the assets From a97207430682c45af8a1afb1228f2925cc9782c4 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 25 Oct 2015 17:09:57 +0800 Subject: [PATCH 058/305] environment assets config --- config/environments/development.rb | 4 ---- config/environments/production.rb | 2 +- config/environments/test.rb | 6 ++++++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index c4d3540e..cd440097 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -40,10 +40,6 @@ Metamaps::Application.configure do # Print deprecation notices to the Rails logger config.active_support.deprecation = :log - # Compress assets - config.assets.compile = true - config.assets.compress = false - # Expands the lines which load the assets config.assets.debug = true end diff --git a/config/environments/production.rb b/config/environments/production.rb index a515b4ca..192f631d 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -15,7 +15,7 @@ Metamaps::Application.configure do # Disable Rails's static asset server (Apache or nginx will already do this) config.serve_static_files = true - config.assets.compile = true + config.assets.compile = false # Compress JavaScripts and CSS config.assets.compress = true diff --git a/config/environments/test.rb b/config/environments/test.rb index 18c17296..73003840 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -30,4 +30,10 @@ Metamaps::Application.configure do # Print deprecation notices to the stderr config.active_support.deprecation = :stderr + + #assets config + config.assets.compile = true + config.assets.compress = false + config.assets.debug = false + config.assets.digest = false end From d8dffad38a0fc882c60bcf3b648ed2f1b894b5bd Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 25 Oct 2015 17:14:56 +0800 Subject: [PATCH 059/305] metacodes#index map in asset_path --- app/controllers/metacodes_controller.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/metacodes_controller.rb b/app/controllers/metacodes_controller.rb index 1e5049f1..b0978812 100644 --- a/app/controllers/metacodes_controller.rb +++ b/app/controllers/metacodes_controller.rb @@ -5,8 +5,10 @@ class MetacodesController < ApplicationController # GET /metacodes # GET /metacodes.json def index - @metacodes = Metacode.order("name").all + @metacodes.map do |metacode| + metacode.icon = asset_path(metacode.icon) + end respond_to do |format| format.html { From 60898fadb239a226df6ef35ec78a2c28c96e76a9 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 26 Oct 2015 11:53:50 +0800 Subject: [PATCH 060/305] binding_of_caller gem for devel debug --- Gemfile | 1 + Gemfile.lock | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/Gemfile b/Gemfile index 24fe17fe..b1333275 100644 --- a/Gemfile +++ b/Gemfile @@ -47,5 +47,6 @@ group :development, :test do gem 'pry-rails' gem 'pry-byebug' gem 'better_errors' + gem 'binding_of_caller' gem 'quiet_assets' end diff --git a/Gemfile.lock b/Gemfile.lock index bff8145f..ca22d0b8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -51,6 +51,8 @@ GEM coderay (>= 1.0.0) erubis (>= 2.6.6) rack (>= 0.9.0) + binding_of_caller (0.7.2) + debug_inspector (>= 0.0.1) builder (3.2.2) byebug (4.0.5) columnize (= 0.9.0) @@ -68,6 +70,7 @@ GEM execjs coffee-script-source (1.9.1.1) columnize (0.9.0) + debug_inspector (0.0.2) devise (3.5.2) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -207,6 +210,7 @@ DEPENDENCIES aws-sdk best_in_place better_errors + binding_of_caller cancancan coffee-rails devise From bdb5623a02d136ae8351a7e0fdfb148a8be1a052 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 26 Oct 2015 11:53:54 +0800 Subject: [PATCH 061/305] woot fix metacodes --- app/models/metacode.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/models/metacode.rb b/app/models/metacode.rb index 03b0f0c0..b404ad5f 100644 --- a/app/models/metacode.rb +++ b/app/models/metacode.rb @@ -13,4 +13,15 @@ class Metacode < ActiveRecord::Base return true if self.metacode_sets.include? metacode_set return false end + + def asset_path_icon + ActionController::Base.helpers.asset_path icon + end + + #output json with asset_paths merged in + def as_json(options) + json = super(options.merge!(methods: :asset_path_icon)) + json["icon"] = json["asset_path_icon"] + json.except("asset_path_icon") + end end From 684dcd8d336568e0d23325d9945f53f7ab480761 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 26 Oct 2015 12:12:07 +0800 Subject: [PATCH 062/305] only run metacode.icon through asset_path if the path doesn't start with http --- app/models/metacode.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/metacode.rb b/app/models/metacode.rb index b404ad5f..a545a428 100644 --- a/app/models/metacode.rb +++ b/app/models/metacode.rb @@ -21,7 +21,7 @@ class Metacode < ActiveRecord::Base #output json with asset_paths merged in def as_json(options) json = super(options.merge!(methods: :asset_path_icon)) - json["icon"] = json["asset_path_icon"] + json["icon"] = json["asset_path_icon"] if json["icon"].start_with?('http') json.except("asset_path_icon") end end From f4987bffc03be8bc2e4192ed9387b5312f126fab Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 26 Oct 2015 12:22:40 +0800 Subject: [PATCH 063/305] change css files to use asset_data_uri --- app/assets/stylesheets/application.css.erb | 72 +++++++++---------- app/assets/stylesheets/base.css.erb | 40 +++++------ app/assets/stylesheets/clean.css.erb | 40 +++++------ .../{jquery-ui.css => jquery-ui.css.erb} | 34 ++++----- .../stylesheets/jquery.mCustomScrollbar.css | 4 +- 5 files changed, 95 insertions(+), 95 deletions(-) rename app/assets/stylesheets/{jquery-ui.css => jquery-ui.css.erb} (88%) diff --git a/app/assets/stylesheets/application.css.erb b/app/assets/stylesheets/application.css.erb index ed4ec2ac..f2093a0f 100644 --- a/app/assets/stylesheets/application.css.erb +++ b/app/assets/stylesheets/application.css.erb @@ -78,7 +78,7 @@ html { } body { - background: #d8d9da url(shattered_@2X.png); + background: #d8d9da url(<%= asset_data_uri('shattered_@2X.png') %>); font-family: 'din-medium', helvetica, sans-serif; color: #424242; -moz-osx-font-smoothing: grayscale; @@ -300,7 +300,7 @@ input[type="submit"]:active { display: none; width: 40px; height: 40px; - background-image: url(photo.png); + background-image: url(<%= asset_data_uri('photo.png') %>); position: absolute; top: 22px; left: 22px; @@ -413,7 +413,7 @@ input[type="submit"]:active { } .accountName:hover .nameEdit:after { - background:url(edit.png)no-repeat; + background: url(<%= asset_data_uri('edit.png') %>) no-repeat; content:" "; position:absolute; width:25px; @@ -547,7 +547,7 @@ input[type="submit"]:active { display: block; height: 16px; width: 16px; - background-image: url(metacodesettings_sprite.png); + background-image: url(<%= asset_data_uri('metacodesettings_sprite.png') %>); position: absolute; z-index: 2; top: 20px; @@ -652,7 +652,7 @@ label { box-shadow: 6px 6px 8px rgba(0, 0, 0, 0.4); } .headertop .tab { - background: url(tab.png) no-repeat 0 0; + background: url(<%= asset_data_uri('tab.png') %>) no-repeat 0 0; position: absolute; top: -11px; right: -2px; @@ -719,7 +719,7 @@ label { .accountInnerArrow { width:16px; height:16px; - background-image: url(arrowdown_sprite.png); + background-image: url(<%= asset_data_uri('arrowdown_sprite.png') %>); background-repeat: no-repeat; position:absolute; top: 8px; @@ -783,7 +783,7 @@ label { position:absolute; pointer-events:none; background-repeat:no-repeat; - background-image: url(user_sprite.png); + background-image: url(<%= asset_data_uri('user_sprite.png') %>); } .accountSettings .accountIcon { background-position: 0 0; @@ -1145,7 +1145,7 @@ h3.realtimeBoxTitle { position: absolute; top: 4px; right: 0; - background-image: url('junto24_sprite.png'); + background-image: url(<%= asset_data_uri(''junto24_sprite.png'') %>); } .realtimeMapperList .littleRtOff .littleJuntoIcon { background-position: 0 0; @@ -1222,7 +1222,7 @@ h3.realtimeBoxTitle { position: absolute; top: 0; left: 4px; - background-image: url(context_sprite.png); + background-image: url(<%= asset_data_uri('context_sprite.png') %>); background-repeat: no-repeat; width: 24px; height: 24px; @@ -1238,7 +1238,7 @@ h3.realtimeBoxTitle { } .rightclickmenu .rc-center .rc-icon { - background-image: url(context_topicview_sprite.png); + background-image: url(<%= asset_data_uri('context_topicview_sprite.png') %>); } .rightclickmenu .rc-popout .rc-icon { background-position: 0 -72px; @@ -1251,7 +1251,7 @@ h3.realtimeBoxTitle { } .rightclickmenu .rc-siblings .rc-icon { background-position: 0 -24px; - background-image: url(context_topicview_sprite.png); + background-image: url(<%= asset_data_uri('context_topicview_sprite.png') %>); } .rightclickmenu .expandLi { position: absolute; @@ -1259,7 +1259,7 @@ h3.realtimeBoxTitle { right: 8px; width: 16px; height: 16px; - background-image: url(arrowright_sprite.png); + background-image: url(<%= asset_data_uri('arrowright_sprite.png') %>); background-repeat: no-repeat; background-position: 0 -32px; } @@ -1294,7 +1294,7 @@ h3.realtimeBoxTitle { position: absolute; top: 0; left: 4px; - background-image: url(permissions32_sprite.png); + background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>); background-size: 72px 48px; background-repeat: no-repeat; width: 24px; @@ -1339,7 +1339,7 @@ h3.realtimeBoxTitle { right: 4px; width: 16px; height: 16px; - background-image: url(arrowright_sprite.png); + background-image: url(<%= asset_data_uri('arrowright_sprite.png') %>); background-repeat: no-repeat; background-position: 0 -32px; } @@ -1419,7 +1419,7 @@ h3.realtimeBoxTitle { top: 8px; right: 8px; background-repeat: no-repeat; - background-image: url(arrowright_sprite.png); + background-image: url(<%= asset_data_uri('arrowright_sprite.png') %>); background-position: 0 -32px; } #new_topic .tt-suggestion:hover .expandTopicMetadata, @@ -1445,7 +1445,7 @@ h3.realtimeBoxTitle { width: 32px; height: 32px; background-repeat: no-repeat; - background-image: url(permissions32_sprite.png); + background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>); position: absolute; bottom: 4px; right: 4px; @@ -1465,7 +1465,7 @@ h3.realtimeBoxTitle { #new_topic .topicNumMaps { height: 14px; padding: 1px 0 1px 32px; - background-image: url(metamap16.png); + background-image: url(<%= asset_data_uri('metamap16.png') %>); background-repeat: no-repeat; background-position: 8px 0; position: absolute; @@ -1474,7 +1474,7 @@ h3.realtimeBoxTitle { #new_topic .topicNumSynapses { height: 14px; padding: 1px 0 1px 32px; - background-image: url(synapse16.png); + background-image: url(<%= asset_data_uri('synapse16.png') %>); background-repeat: no-repeat; background-position: 8px 0; position: absolute; @@ -1564,7 +1564,7 @@ h3.realtimeBoxTitle { width: 32px; height: 32px; background-repeat: no-repeat; - background-image: url(permissions32_sprite.png); + background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>); } /* map info box */ /* map info box */ @@ -1604,7 +1604,7 @@ h3.realtimeBoxTitle { resize: none; } .mapInfoBox.canEdit #mapInfoName:hover { - background-image: url(edit.png); + background-image: url(<%= asset_data_uri('edit.png') %>); background-repeat: no-repeat; background-position: bottom right; cursor: text; @@ -1758,11 +1758,11 @@ h3.realtimeBoxTitle { } .mapTopics { - background-image: url(topic32.png); + background-image: url(<%= asset_data_uri('topic32.png') %>); background-position: 13px center; } .mapSynapses { - background-image: url(synapse32padded.png); + background-image: url(<%= asset_data_uri('synapse32padded.png') %>); background-position: 13px center; } .mapInfoBox .mapPermission { @@ -1772,7 +1772,7 @@ h3.realtimeBoxTitle { padding: 0; margin: 8px 30px 8px 10px; position: relative; - background-image: url(permissions32_sprite.png); + background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>); } .mapInfoBox .mapPermission.commons { background-position: 0 0; @@ -1784,12 +1784,12 @@ h3.realtimeBoxTitle { background-position: -32px 0; } .yourMap .mapPermission:hover { - background-image: url(arrowperms_sprite.png); + background-image: url(<%= asset_data_uri('arrowperms_sprite.png') %>); cursor: pointer; background-position: -32px 0; } .yourMap .mapPermission.minimize { - background-image: url(arrowperms_sprite.png) !important; + background-image: url(<%= asset_data_uri('arrowperms_sprite.png') %>) !important; cursor: pointer; background-position: 0 0; } @@ -1804,7 +1804,7 @@ h3.realtimeBoxTitle { width: 32px; height: 32px; background-repeat: no-repeat; - background-image: url(permissions32_sprite.png); + background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>); } .mapInfoBox .mapPermission .permissionSelect .commons { background-position: 0 0; @@ -1851,7 +1851,7 @@ h3.realtimeBoxTitle { width: 340px; } .mapInfoBox.canEdit .mapInfoDesc:hover { - background-image: url(edit.png); + background-image: url(<%= asset_data_uri('edit.png') %>); background-position: top right; background-repeat: no-repeat; cursor: text; @@ -1896,7 +1896,7 @@ h3.realtimeBoxTitle { .mapInfoShareIcon { width: 24px; height: 24px; - background-image: url(share_sprite_mapinfo.png); + background-image: url(<%= asset_data_uri('share_sprite_mapinfo.png') %>); background-repeat: no-repeat; margin: 4px auto 0; background-position: 0 -24px; @@ -1917,7 +1917,7 @@ and it won't be important on password protected instances */ width: 16px; height: 16px; margin: 8px auto 0; - background-image: url(remove_mapinfo_sprite.png); + background-image: url(<%= asset_data_uri('remove_mapinfo_sprite.png') %>); background-repeat: no-repeat; background-position: -16px 0; } @@ -1978,7 +1978,7 @@ and it won't be important on password protected instances */ border: solid 2px #000; } #lightbox_overlay #lightbox_main a#lightbox_close { - background-image: url(xlightbox.png); + background-image: url(<%= asset_data_uri('xlightbox.png') %>); cursor: pointer; height: 32px; outline-style: none; @@ -2077,7 +2077,7 @@ and it won't be important on password protected instances */ color: #00bcd4; } .lightbox_links .lightboxAboutIcon { - background-image: url(about_sprite.png); + background-image: url(<%= asset_data_uri('about_sprite.png') %>); background-repeat: no-repeat; width:32px; height:32px; @@ -2497,7 +2497,7 @@ and it won't be important on password protected instances */ position: relative; } .new_map .mapPermIcon { - background-image: url(permissions64sprite.png); + background-image: url(<%= asset_data_uri('permissions64sprite.png') %>); background-repeat: no-repeat; width:64px; height:64px; @@ -2612,7 +2612,7 @@ and it won't be important on password protected instances */ } #zoomIn { - background: #424242 url(zoom_sprite.png) no-repeat; + background: #424242 url(<%= asset_data_uri('zoom_sprite.png') %>) no-repeat; width: 22px; height: 22px; display: inline-block; @@ -2624,7 +2624,7 @@ and it won't be important on password protected instances */ } #zoomOut { - background: #424242 url(zoom_sprite.png) no-repeat; + background: #424242 url(<%= asset_data_uri('zoom_sprite.png') %>) no-repeat; width: 22px; height: 22px; display: inline-block; @@ -2636,7 +2636,7 @@ and it won't be important on password protected instances */ } #centerMap { - background: #424242 url(extents_sprite.png) no-repeat; + background: #424242 url(<%= asset_data_uri('extents_sprite.png') %>) no-repeat; width: 22px; height: 22px; display: inline-block; @@ -2941,7 +2941,7 @@ and it won't be important on password protected instances */ .compassArrow { display: none; background-repeat: no-repeat; - background-image: url(compass_arrow.png); + background-image: url(<%= asset_data_uri('compass_arrow.png') %>); width: 48px; height: 32px; position: absolute; diff --git a/app/assets/stylesheets/base.css.erb b/app/assets/stylesheets/base.css.erb index 858163ad..216fa04d 100644 --- a/app/assets/stylesheets/base.css.erb +++ b/app/assets/stylesheets/base.css.erb @@ -85,7 +85,7 @@ padding: 0 16px; } .canEdit #titleActivator:hover { - background-image: url(edit.png); + background-image: url(<%= asset_data_uri('edit.png') %>); background-repeat: no-repeat; background-position: bottom right; cursor: text; @@ -148,7 +148,7 @@ margin-right: 8px; } .canEdit .CardOnGraph .best_in_place_desc:hover { - background-image: url(edit.png); + background-image: url(<%= asset_data_uri('edit.png') %>); background-position: top right; background-repeat: no-repeat; cursor: text; @@ -244,7 +244,7 @@ left: 0; width: 32px; height: 32px; - background-image: url(map32_sprite.png); + background-image: url(<%= asset_data_uri('map32_sprite.png') %>); background-repeat: no-repeat; background-position: 0 0; cursor: pointer; @@ -335,7 +335,7 @@ left: 0; width: 32px; height: 32px; - background-image: url(synapse32_sprite.png); + background-image: url(<%= asset_data_uri('synapse32_sprite.png') %>); background-repeat: no-repeat; background-position: 0 0; } @@ -382,7 +382,7 @@ min-width: 32px; margin-top: 8px; margin-left: 8px; - background-image: url(permissions32_sprite.png); + background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>); background-position: 0 0; } .mapPerm.co { @@ -396,12 +396,12 @@ } .yourTopic .mapPerm:hover, .yourEdge .mapPerm:hover { - background-image: url(arrowperms_sprite.png); + background-image: url(<%= asset_data_uri('arrowperms_sprite.png') %>); background-position: -32px 0; cursor:pointer; } .yourTopic .mapPerm.minimize, .yourEdge .mapPerm.minimize { - background-image: url(arrowperms_sprite.png) !important; + background-image: url(<%= asset_data_uri('arrowperms_sprite.png') %>) !important; background-position: 0 0; cursor: pointer; } @@ -417,7 +417,7 @@ cursor: pointer; height: 32px; background-repeat: no-repeat; background-position: 0 0; - background-image: url(permissions32_sprite.png); + background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>); } .mapPerm .permissionSelect .commons { background-position: 0 0; @@ -462,7 +462,7 @@ cursor: pointer; right: 16px; width: 16px; height: 16px; - background-image: url(arrowright_sprite.png); + background-image: url(<%= asset_data_uri('arrowright_sprite.png') %>); background-repeat: no-repeat; background-position: 0 -32px; } @@ -542,7 +542,7 @@ background-color: #E0E0E0; right: 8px; width: 16px; height: 16px; - background-image: url(arrowright_sprite.png); + background-image: url(<%= asset_data_uri('arrowright_sprite.png') %>); background-repeat: no-repeat; background-position: 0 -32px; } @@ -648,10 +648,10 @@ background-color: #E0E0E0; left: 12px; } #linkIcon { - background-image: url(link_sprite.png); + background-image: url(<%= asset_data_uri('link_sprite.png') %>); } #uploadIcon { - background-image: url(upload_sprite.png); + background-image: url(<%= asset_data_uri('upload_sprite.png') %>); } #addlink:hover #linkIcon, #addupload:hover #uploadIcon { background-position: 0 -24px; @@ -690,7 +690,7 @@ font-family: 'din-regular', helvetica, sans-serif; height: 24px; background-repeat: no-repeat; background-position: 0 0; - background-image: url(link_sprite.png); + background-image: url(<%= asset_data_uri('link_sprite.png') %>); pointer-events: none; z-index: 1; } @@ -703,7 +703,7 @@ font-family: 'din-regular', helvetica, sans-serif; height: 32px; cursor: pointer; float:none; - background-image: url(remove.png); + background-image: url(<%= asset_data_uri('remove.png') %>); background-repeat: no-repeat; background-position: center center; } @@ -781,7 +781,7 @@ font-family: 'din-regular', helvetica, sans-serif; } #editSynUpperBar { - background: #FFFFFF url(synapse32.png) no-repeat 8px center; + background: #FFFFFF url(<%= asset_data_uri('synapse32.png') %>) no-repeat 8px center; min-height: 48px; height: 48px; border-bottom: 1px solid #222222; @@ -814,7 +814,7 @@ font-family: 'din-regular', helvetica, sans-serif; } .canEdit #edit_synapse_desc:hover { - background-image: url(edit.png); + background-image: url(<%= asset_data_uri('edit.png') %>); background-repeat: no-repeat; background-position: 164px center; cursor: text; @@ -841,7 +841,7 @@ font-family: 'din-regular', helvetica, sans-serif; height: 24px; top: 12px; right: 8px; - background-image: url(arrowdown_sprite.png); + background-image: url(<%= asset_data_uri('arrowdown_sprite.png') %>); background-repeat: no-repeat; background-position: 4px -12px; } @@ -923,11 +923,11 @@ font-family: 'din-regular', helvetica, sans-serif; background-repeat: no-repeat; } #edit_synapse_right { - background-image: url(synapsedirectionright_sprite.png); + background-image: url(<%= asset_data_uri('synapsedirectionright_sprite.png') %>); right: 16px; } #edit_synapse_left { - background-image: url(synapsedirectionleft_sprite.png); + background-image: url(<%= asset_data_uri('synapsedirectionleft_sprite.png') %>); right: 56px; } #edit_synapse_left.checked, #edit_synapse_right.checked { @@ -1103,7 +1103,7 @@ font-family: 'din-regular', helvetica, sans-serif; } .mapperMetadata { - background: url(profile_card_sprite.png) no-repeat center 0; + background: url(<%= asset_data_uri('profile_card_sprite.png') %>) no-repeat center 0; height: 64px; width: 160px; margin: 16px auto 0; diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index 008e8c9d..fa0c9758 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -134,7 +134,7 @@ width: 40px; height: 32px; background-color: #757575; - background-image: url(home_light.png); + background-image: url(<%= asset_data_uri('home_light.png') %>); background-repeat: no-repeat; background-position: center center; border-top-left-radius: 2px; @@ -142,7 +142,7 @@ float:left; } .homeButton:hover { - background-image: url(home_light.png); + background-image: url(<%= asset_data_uri('home_light.png') %>); } .homeButton a { display:block; @@ -176,7 +176,7 @@ border-top-right-radius: 2px; border-bottom-right-radius: 2px; height: 32px; - background: #4fb5c0 url('search.png') no-repeat center center; + background: #4fb5c0 url(<%= asset_data_uri(''search.png'') %>) no-repeat center center; background-size: 32px 32px; cursor: pointer; } @@ -288,7 +288,7 @@ .sidebarSearch .tt-dropdown-menu .minimizeResults, .sidebarSearch .tt-dropdown-menu .maximizeResults { width: 32px; height: 32px; - background-image: url(arrowpermswhite_sprite.png); + background-image: url(<%= asset_data_uri('arrowpermswhite_sprite.png') %>); background-repeat: no-repeat; cursor: pointer; position: absolute; @@ -425,7 +425,7 @@ display:none; width: 24px; height: 24px; - background: url(addtopic_sprite.png); + background: url(<%= asset_data_uri('addtopic_sprite.png') %>); background-repeat: no-repeat; background-size: 48px 24px; top: 12px; @@ -442,7 +442,7 @@ .sidebarSearch div.topicCount { width: 24px; height: 24px; - background: url(topic16.png); + background: url(<%= asset_data_uri('topic16.png') %>); background-repeat: no-repeat; background-position: 0 center; top: 0; @@ -455,7 +455,7 @@ .sidebarSearch div.mapCount { width: 24px; height: 24px; - background: url(metamap16.png); + background: url(<%= asset_data_uri('metamap16.png') %>); background-repeat: no-repeat; background-position: 0 center; left: 0; @@ -466,7 +466,7 @@ .sidebarSearch div.synapseCount { width: 24px; height: 24px; - background: url(synapse16.png); + background: url(<%= asset_data_uri('synapse16.png') %>); background-repeat: no-repeat; background-position: 0 center; top: 24px; @@ -573,7 +573,7 @@ .sidebarSearch div.mapPermission { width: 24px; height: 24px; - background-image: url(permissions32_sprite.png); + background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>); background-repeat: no-repeat; background-size: 72px 48px !important; top: 24px; @@ -665,7 +665,7 @@ .upperRightIcon { width: 32px; height: 32px; - background-image: url('topright_sprite.png'); + background-image: url(<%= asset_data_uri(''topright_sprite.png'') %>); background-repeat: no-repeat; cursor: pointer; } @@ -816,12 +816,12 @@ } .fullWidthWrapper.withPartners { - background: url(homepage_bg_fade.png) no-repeat center -300px; + background: url(<%= asset_data_uri('homepage_bg_fade.png') %>) no-repeat center -300px; } .homeWrapper.homePartners { padding: 64px 0 280px; height: 96px; - background: url(partner_logos.png) no-repeat 0 64px; + background: url(<%= asset_data_uri('partner_logos.png') %>) no-repeat 0 64px; } .github-fork-ribbon-wrapper { @@ -863,7 +863,7 @@ cursor: pointer; } .openCheatsheet { - background-image: url('help_sprite.png'); + background-image: url(<%= asset_data_uri(''help_sprite.png'') %>); background-repeat:no-repeat; } .openCheatsheet:hover { @@ -872,7 +872,7 @@ .mapInfoIcon { position: relative; top: 56px; /* puts it just offscreen */ - background-image: url('mapinfo_sprite.png'); + background-image: url(<%= asset_data_uri(''mapinfo_sprite.png'') %>); background-repeat:no-repeat; } .mapInfoIcon:hover { @@ -931,7 +931,7 @@ .zoomExtents { margin-bottom:5px; border-radius: 2px; - background-image: url(extents_sprite.png); + background-image: url(<%= asset_data_uri('extents_sprite.png') %>); } .zoomExtents:hover { @@ -1076,7 +1076,7 @@ } .zoomIn { - background-image: url(zoom_sprite.png); + background-image: url(<%= asset_data_uri('zoom_sprite.png') %>); background-position: 0 /…0; border-top-left-radius: 2px; border-top-right-radius: 2px; @@ -1085,7 +1085,7 @@ background-position: -32px 0; } .zoomOut { - background-image: url(zoom_sprite.png); + background-image: url(<%= asset_data_uri('zoom_sprite.png') %>); background-position:0 -32px; border-bottom-left-radius: 2px; border-bottom-right-radius: 2px; @@ -1163,15 +1163,15 @@ left:5px; } .exploreMapsCenter .myMaps .exploreMapsIcon { - background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); + background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); background-position: 0 0; } .exploreMapsCenter .activeMaps .exploreMapsIcon { - background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); + background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); background-position: -32px 0; } .exploreMapsCenter .featuredMaps .exploreMapsIcon { - background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); + background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); background-position: -64px 0; } .myMaps:hover .exploreMapsIcon, .myMaps.active .exploreMapsIcon { diff --git a/app/assets/stylesheets/jquery-ui.css b/app/assets/stylesheets/jquery-ui.css.erb similarity index 88% rename from app/assets/stylesheets/jquery-ui.css rename to app/assets/stylesheets/jquery-ui.css.erb index c89a95ff..88113a6e 100644 --- a/app/assets/stylesheets/jquery-ui.css +++ b/app/assets/stylesheets/jquery-ui.css.erb @@ -243,25 +243,25 @@ body .ui-tooltip { border-width: 2px; } .ui-widget { font-family: Verdana,Arial,sans-serif/*{ffDefault}*/; font-size: 1.1em/*{fsDefault}*/; } .ui-widget .ui-widget { font-size: 1em; } .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Verdana,Arial,sans-serif/*{ffDefault}*/; font-size: 1em; } -.ui-widget-content { border: 1px solid #aaaaaa/*{borderColorContent}*/; background: #ffffff/*{bgColorContent}*/ url(ui-bg_flat_75_ffffff_40x100.png)/*{bgImgUrlContent}*/ 50%/*{bgContentXPos}*/ 50%/*{bgContentYPos}*/ repeat-x/*{bgContentRepeat}*/; color: #222222/*{fcContent}*/; } +.ui-widget-content { border: 1px solid #aaaaaa/*{borderColorContent}*/; background: #ffffff/*{bgColorContent}*/ url(<%= asset_data_uri('ui-bg_flat_75_ffffff_40x100.png') %>)/*{bgImgUrlContent}*/ 50%/*{bgContentXPos}*/ 50%/*{bgContentYPos}*/ repeat-x/*{bgContentRepeat}*/; color: #222222/*{fcContent}*/; } .ui-widget-content a { color: #222222/*{fcContent}*/; } -.ui-widget-header { border: 1px solid #aaaaaa/*{borderColorHeader}*/; background: #cccccc/*{bgColorHeader}*/ url(images/ui-bg_highlight-soft_75_cccccc_1x100.png)/*{bgImgUrlHeader}*/ 50%/*{bgHeaderXPos}*/ 50%/*{bgHeaderYPos}*/ repeat-x/*{bgHeaderRepeat}*/; color: #222222/*{fcHeader}*/; font-weight: bold; } +.ui-widget-header { border: 1px solid #aaaaaa/*{borderColorHeader}*/; background: #cccccc/*{bgColorHeader}*/ url(<%= asset_data_uri('images/ui-bg_highlight-soft_75_cccccc_1x100.png') %>)/*{bgImgUrlHeader}*/ 50%/*{bgHeaderXPos}*/ 50%/*{bgHeaderYPos}*/ repeat-x/*{bgHeaderRepeat}*/; color: #222222/*{fcHeader}*/; font-weight: bold; } .ui-widget-header a { color: #222222/*{fcHeader}*/; } /* Interaction states ----------------------------------*/ -.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3/*{borderColorDefault}*/; background: #e6e6e6/*{bgColorDefault}*/ url(images/ui-bg_glass_75_e6e6e6_1x400.png)/*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #555555/*{fcDefault}*/; } +.ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #d3d3d3/*{borderColorDefault}*/; background: #e6e6e6/*{bgColorDefault}*/ url(<%= asset_data_uri('images/ui-bg_glass_75_e6e6e6_1x400.png') %>)/*{bgImgUrlDefault}*/ 50%/*{bgDefaultXPos}*/ 50%/*{bgDefaultYPos}*/ repeat-x/*{bgDefaultRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #555555/*{fcDefault}*/; } .ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #555555/*{fcDefault}*/; text-decoration: none; } -.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 0px solid #999999/*{borderColorHover}*/; background: #dadada/*{bgColorHover}*/ url(images/ui-bg_glass_75_dadada_1x400.png)/*{bgImgUrlHover}*/ 50%/*{bgHoverXPos}*/ 50%/*{bgHoverYPos}*/ repeat-x/*{bgHoverRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcHover}*/; } +.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 0px solid #999999/*{borderColorHover}*/; background: #dadada/*{bgColorHover}*/ url(<%= asset_data_uri('images/ui-bg_glass_75_dadada_1x400.png') %>)/*{bgImgUrlHover}*/ 50%/*{bgHoverXPos}*/ 50%/*{bgHoverYPos}*/ repeat-x/*{bgHoverRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcHover}*/; } .ui-state-hover a, .ui-state-hover a:hover, .ui-state-hover a:link, .ui-state-hover a:visited { color: #212121/*{fcHover}*/; text-decoration: none; } -.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa/*{borderColorActive}*/; background: #ffffff/*{bgColorActive}*/ url(images/ui-bg_glass_65_ffffff_1x400.png)/*{bgImgUrlActive}*/ 50%/*{bgActiveXPos}*/ 50%/*{bgActiveYPos}*/ repeat-x/*{bgActiveRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcActive}*/; } +.ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { border: 1px solid #aaaaaa/*{borderColorActive}*/; background: #ffffff/*{bgColorActive}*/ url(<%= asset_data_uri('images/ui-bg_glass_65_ffffff_1x400.png') %>)/*{bgImgUrlActive}*/ 50%/*{bgActiveXPos}*/ 50%/*{bgActiveYPos}*/ repeat-x/*{bgActiveRepeat}*/; font-weight: normal/*{fwDefault}*/; color: #212121/*{fcActive}*/; } .ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #212121/*{fcActive}*/; text-decoration: none; } /* Interaction Cues ----------------------------------*/ -.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #fcefa1/*{borderColorHighlight}*/; background: #fbf9ee/*{bgColorHighlight}*/ url(images/ui-bg_glass_55_fbf9ee_1x400.png)/*{bgImgUrlHighlight}*/ 50%/*{bgHighlightXPos}*/ 50%/*{bgHighlightYPos}*/ repeat-x/*{bgHighlightRepeat}*/; color: #363636/*{fcHighlight}*/; } +.ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight {border: 1px solid #fcefa1/*{borderColorHighlight}*/; background: #fbf9ee/*{bgColorHighlight}*/ url(<%= asset_data_uri('images/ui-bg_glass_55_fbf9ee_1x400.png') %>)/*{bgImgUrlHighlight}*/ 50%/*{bgHighlightXPos}*/ 50%/*{bgHighlightYPos}*/ repeat-x/*{bgHighlightRepeat}*/; color: #363636/*{fcHighlight}*/; } .ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636/*{fcHighlight}*/; } -.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a/*{borderColorError}*/; background: #fef1ec/*{bgColorError}*/ url(images/ui-bg_glass_95_fef1ec_1x400.png)/*{bgImgUrlError}*/ 50%/*{bgErrorXPos}*/ 50%/*{bgErrorYPos}*/ repeat-x/*{bgErrorRepeat}*/; color: #cd0a0a/*{fcError}*/; } +.ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error {border: 1px solid #cd0a0a/*{borderColorError}*/; background: #fef1ec/*{bgColorError}*/ url(<%= asset_data_uri('images/ui-bg_glass_95_fef1ec_1x400.png') %>)/*{bgImgUrlError}*/ 50%/*{bgErrorXPos}*/ 50%/*{bgErrorYPos}*/ repeat-x/*{bgErrorRepeat}*/; color: #cd0a0a/*{fcError}*/; } .ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a/*{fcError}*/; } .ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a/*{fcError}*/; } .ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; } @@ -273,14 +273,14 @@ body .ui-tooltip { border-width: 2px; } ----------------------------------*/ /* states and images */ -.ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png)/*{iconsContent}*/; } -.ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png)/*{iconsContent}*/; } -.ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png)/*{iconsHeader}*/; } -.ui-state-default .ui-icon { background-image: url(images/ui-icons_888888_256x240.png)/*{iconsDefault}*/; } -.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png)/*{iconsHover}*/; } -.ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png)/*{iconsActive}*/; } -.ui-state-highlight .ui-icon {background-image: url(images/ui-icons_2e83ff_256x240.png)/*{iconsHighlight}*/; } -.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(images/ui-icons_cd0a0a_256x240.png)/*{iconsError}*/; } +.ui-icon { width: 16px; height: 16px; background-image: url(<%= asset_data_uri('images/ui-icons_222222_256x240.png') %>)/*{iconsContent}*/; } +.ui-widget-content .ui-icon {background-image: url(<%= asset_data_uri('images/ui-icons_222222_256x240.png') %>)/*{iconsContent}*/; } +.ui-widget-header .ui-icon {background-image: url(<%= asset_data_uri('images/ui-icons_222222_256x240.png') %>)/*{iconsHeader}*/; } +.ui-state-default .ui-icon { background-image: url(<%= asset_data_uri('images/ui-icons_888888_256x240.png') %>)/*{iconsDefault}*/; } +.ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(<%= asset_data_uri('images/ui-icons_454545_256x240.png') %>)/*{iconsHover}*/; } +.ui-state-active .ui-icon {background-image: url(<%= asset_data_uri('images/ui-icons_454545_256x240.png') %>)/*{iconsActive}*/; } +.ui-state-highlight .ui-icon {background-image: url(<%= asset_data_uri('images/ui-icons_2e83ff_256x240.png') %>)/*{iconsHighlight}*/; } +.ui-state-error .ui-icon, .ui-state-error-text .ui-icon {background-image: url(<%= asset_data_uri('images/ui-icons_cd0a0a_256x240.png') %>)/*{iconsError}*/; } /* positioning */ .ui-icon-carat-1-n { background-position: 0 0; } @@ -470,5 +470,5 @@ body .ui-tooltip { border-width: 2px; } .ui-corner-all, .ui-corner-bottom, .ui-corner-right, .ui-corner-br { -moz-border-radius-bottomright: 4px/*{cornerRadius}*/; -webkit-border-bottom-right-radius: 4px/*{cornerRadius}*/; -khtml-border-bottom-right-radius: 4px/*{cornerRadius}*/; border-bottom-right-radius: 4px/*{cornerRadius}*/; } /* Overlays */ -.ui-widget-overlay { background: #aaaaaa/*{bgColorOverlay}*/ url(images/ui-bg_flat_0_aaaaaa_40x100.png)/*{bgImgUrlOverlay}*/ 50%/*{bgOverlayXPos}*/ 50%/*{bgOverlayYPos}*/ repeat-x/*{bgOverlayRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityOverlay}*/; } -.ui-widget-shadow { margin: -8px/*{offsetTopShadow}*/ 0 0 -8px/*{offsetLeftShadow}*/; padding: 8px/*{thicknessShadow}*/; background: #aaaaaa/*{bgColorShadow}*/ url(images/ui-bg_flat_0_aaaaaa_40x100.png)/*{bgImgUrlShadow}*/ 50%/*{bgShadowXPos}*/ 50%/*{bgShadowYPos}*/ repeat-x/*{bgShadowRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityShadow}*/; -moz-border-radius: 8px/*{cornerRadiusShadow}*/; -khtml-border-radius: 8px/*{cornerRadiusShadow}*/; -webkit-border-radius: 8px/*{cornerRadiusShadow}*/; border-radius: 8px/*{cornerRadiusShadow}*/; } +.ui-widget-overlay { background: #aaaaaa/*{bgColorOverlay}*/ url(<%= asset_data_uri('images/ui-bg_flat_0_aaaaaa_40x100.png') %>)/*{bgImgUrlOverlay}*/ 50%/*{bgOverlayXPos}*/ 50%/*{bgOverlayYPos}*/ repeat-x/*{bgOverlayRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityOverlay}*/; } +.ui-widget-shadow { margin: -8px/*{offsetTopShadow}*/ 0 0 -8px/*{offsetLeftShadow}*/; padding: 8px/*{thicknessShadow}*/; background: #aaaaaa/*{bgColorShadow}*/ url(images/<%= asset_data_uri('ui-bg_flat_0_aaaaaa_40x100.png') %>)/*{bgImgUrlShadow}*/ 50%/*{bgShadowXPos}*/ 50%/*{bgShadowYPos}*/ repeat-x/*{bgShadowRepeat}*/; opacity: .3;filter:Alpha(Opacity=30)/*{opacityShadow}*/; -moz-border-radius: 8px/*{cornerRadiusShadow}*/; -khtml-border-radius: 8px/*{cornerRadiusShadow}*/; -webkit-border-radius: 8px/*{cornerRadiusShadow}*/; border-radius: 8px/*{cornerRadiusShadow}*/; } diff --git a/app/assets/stylesheets/jquery.mCustomScrollbar.css b/app/assets/stylesheets/jquery.mCustomScrollbar.css index e318b29c..f799cdc4 100644 --- a/app/assets/stylesheets/jquery.mCustomScrollbar.css +++ b/app/assets/stylesheets/jquery.mCustomScrollbar.css @@ -162,7 +162,7 @@ .mCSB_scrollTools .mCSB_buttonDown, .mCSB_scrollTools .mCSB_buttonLeft, .mCSB_scrollTools .mCSB_buttonRight{ - background-image:url(mCSB_buttons.png); + background-image:url(<%= asset_data_uri('mCSB_buttons.png') %>); background-repeat:no-repeat; opacity:0.4; filter:"alpha(opacity=40)"; -ms-filter:"alpha(opacity=40)"; /* old ie */ @@ -471,4 +471,4 @@ } .mCS-dark-thin>.mCSB_scrollTools .mCSB_buttonRight{ background-position:-80px -56px; -} \ No newline at end of file +} From 47205883a9c7f0be5ba1a965a43bdbc02566d325 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 26 Oct 2015 12:24:17 +0800 Subject: [PATCH 064/305] whoops --- app/assets/stylesheets/application.css.erb | 4 ++-- app/assets/stylesheets/clean.css.erb | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/application.css.erb b/app/assets/stylesheets/application.css.erb index f2093a0f..b671dd80 100644 --- a/app/assets/stylesheets/application.css.erb +++ b/app/assets/stylesheets/application.css.erb @@ -652,7 +652,7 @@ label { box-shadow: 6px 6px 8px rgba(0, 0, 0, 0.4); } .headertop .tab { - background: url(<%= asset_data_uri('tab.png') %>) no-repeat 0 0; + background: url('tab.png') no-repeat 0 0; position: absolute; top: -11px; right: -2px; @@ -1145,7 +1145,7 @@ h3.realtimeBoxTitle { position: absolute; top: 4px; right: 0; - background-image: url(<%= asset_data_uri(''junto24_sprite.png'') %>); + background-image: url(<%= asset_data_uri('junto24_sprite.png') %>); } .realtimeMapperList .littleRtOff .littleJuntoIcon { background-position: 0 0; diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index fa0c9758..94164b6b 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -176,7 +176,7 @@ border-top-right-radius: 2px; border-bottom-right-radius: 2px; height: 32px; - background: #4fb5c0 url(<%= asset_data_uri(''search.png'') %>) no-repeat center center; + background: #4fb5c0 url(<%= asset_data_uri('search.png') %>) no-repeat center center; background-size: 32px 32px; cursor: pointer; } @@ -665,7 +665,7 @@ .upperRightIcon { width: 32px; height: 32px; - background-image: url(<%= asset_data_uri(''topright_sprite.png'') %>); + background-image: url(<%= asset_data_uri('topright_sprite.png') %>); background-repeat: no-repeat; cursor: pointer; } @@ -863,7 +863,7 @@ cursor: pointer; } .openCheatsheet { - background-image: url(<%= asset_data_uri(''help_sprite.png'') %>); + background-image: url(<%= asset_data_uri('help_sprite.png') %>); background-repeat:no-repeat; } .openCheatsheet:hover { @@ -872,7 +872,7 @@ .mapInfoIcon { position: relative; top: 56px; /* puts it just offscreen */ - background-image: url(<%= asset_data_uri(''mapinfo_sprite.png'') %>); + background-image: url(<%= asset_data_uri('mapinfo_sprite.png') %>); background-repeat:no-repeat; } .mapInfoIcon:hover { From f63a2422561fac9e50f86b310dede0e24ec81051 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 26 Oct 2015 12:39:40 +0800 Subject: [PATCH 065/305] whoops again --- app/models/metacode.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/metacode.rb b/app/models/metacode.rb index a545a428..2b23d694 100644 --- a/app/models/metacode.rb +++ b/app/models/metacode.rb @@ -21,7 +21,7 @@ class Metacode < ActiveRecord::Base #output json with asset_paths merged in def as_json(options) json = super(options.merge!(methods: :asset_path_icon)) - json["icon"] = json["asset_path_icon"] if json["icon"].start_with?('http') + json["icon"] = json["asset_path_icon"] unless json["icon"].start_with?('http') json.except("asset_path_icon") end end From 89424dcd7f663e55b1836d0048f278a827488ee5 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 26 Oct 2015 18:28:25 +0800 Subject: [PATCH 066/305] try to fix custom metacode icon path --- app/models/metacode.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/metacode.rb b/app/models/metacode.rb index 2b23d694..5f83cb91 100644 --- a/app/models/metacode.rb +++ b/app/models/metacode.rb @@ -21,7 +21,9 @@ class Metacode < ActiveRecord::Base #output json with asset_paths merged in def as_json(options) json = super(options.merge!(methods: :asset_path_icon)) - json["icon"] = json["asset_path_icon"] unless json["icon"].start_with?('http') + unless json["icon"].start_with?('http') + json["icon"] = json["asset_path_icon"] + end json.except("asset_path_icon") end end From 4245703084afce7c12a39a3877d1df6b4313952a Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 26 Oct 2015 20:16:49 +0800 Subject: [PATCH 067/305] see if we can make missing-map.png show up --- app/models/map.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/map.rb b/app/models/map.rb index eab88814..5ed9b32a 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -12,7 +12,7 @@ class Map < ActiveRecord::Base :thumb => ['188x126#', :png] #:full => ['940x630#', :png] }, - :default_url => ActionController::Base.helpers.asset_path('missing-map.png') + :default_url => ActionController::Base.helpers.asset_path('images/missing-map.png') # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :screenshot, :content_type => /\Aimage\/.*\Z/ From 0b96171aa3e6e86566bffd700abaa0cac37c8604 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 26 Oct 2015 20:41:37 +0800 Subject: [PATCH 068/305] bugfix --- app/controllers/metacodes_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/metacodes_controller.rb b/app/controllers/metacodes_controller.rb index b0978812..7a14f2a8 100644 --- a/app/controllers/metacodes_controller.rb +++ b/app/controllers/metacodes_controller.rb @@ -7,7 +7,7 @@ class MetacodesController < ApplicationController def index @metacodes = Metacode.order("name").all @metacodes.map do |metacode| - metacode.icon = asset_path(metacode.icon) + metacode.icon = ActionController::Base.helpers.asset_path(metacode.icon) end respond_to do |format| From f0c0dc48d8ce451f57e7494d3a45de2a14613d1a Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 26 Oct 2015 21:01:13 +0800 Subject: [PATCH 069/305] add highlight to typeahead --- app/assets/javascripts/src/Metamaps.js.erb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index 9846f4ea..84fd38bc 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -678,6 +678,7 @@ Metamaps.Create = { // initialize the autocomplete results for the metacode spinner $('#topic_name').typeahead( { + highlight: true, minLength: 2, }, [{ @@ -767,6 +768,7 @@ Metamaps.Create = { // initialize the autocomplete results for synapse creation $('#synapse_desc').typeahead( { + highlight: true, minLength: 2, }, [{ From 204544dc224a6d2c8da0178777a881393958a866 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 26 Oct 2015 21:04:07 +0800 Subject: [PATCH 070/305] metacode asset_path calls --- app/helpers/topics_helper.rb | 2 +- app/models/metacode.rb | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index 79945b6f..362a5f46 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -10,7 +10,7 @@ module TopicsHelper topic['value'] = t.name topic['description'] = t.desc.truncate(70) # make this return matched results topic['type'] = t.metacode.name - topic['typeImageURL'] = asset_path(t.metacode.icon) + topic['typeImageURL'] = t.metacode.asset_path_icon topic['permission'] = t.permission topic['mapCount'] = t.maps.count topic['synapseCount'] = t.synapses.count diff --git a/app/models/metacode.rb b/app/models/metacode.rb index 5f83cb91..a1184e9c 100644 --- a/app/models/metacode.rb +++ b/app/models/metacode.rb @@ -15,15 +15,17 @@ class Metacode < ActiveRecord::Base end def asset_path_icon - ActionController::Base.helpers.asset_path icon + if icon.start_with?('http') + icon + else + ActionController::Base.helpers.asset_path icon + end end #output json with asset_paths merged in def as_json(options) json = super(options.merge!(methods: :asset_path_icon)) - unless json["icon"].start_with?('http') - json["icon"] = json["asset_path_icon"] - end + json["icon"] = json["asset_path_icon"] json.except("asset_path_icon") end end From 4b6e33f98353c7bd2dd96c673a6a31a3b1d444e4 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 26 Oct 2015 21:12:18 +0800 Subject: [PATCH 071/305] does this fix missing-map.png? --- app/models/map.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/map.rb b/app/models/map.rb index 5ed9b32a..2e63dec4 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -12,7 +12,7 @@ class Map < ActiveRecord::Base :thumb => ['188x126#', :png] #:full => ['940x630#', :png] }, - :default_url => ActionController::Base.helpers.asset_path('images/missing-map.png') + :default_url => ApplicationController.helpers.asset_path('missing-map.png') # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :screenshot, :content_type => /\Aimage\/.*\Z/ From 2cbf1cad7f9f58ee457c05a63ff61d88d44fcaab Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 27 Oct 2015 18:16:35 +0800 Subject: [PATCH 072/305] fix --- app/models/map.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/map.rb b/app/models/map.rb index 2e63dec4..eab88814 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -12,7 +12,7 @@ class Map < ActiveRecord::Base :thumb => ['188x126#', :png] #:full => ['940x630#', :png] }, - :default_url => ApplicationController.helpers.asset_path('missing-map.png') + :default_url => ActionController::Base.helpers.asset_path('missing-map.png') # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :screenshot, :content_type => /\Aimage\/.*\Z/ From 8e427ea8f26c5eb4f6d0c08f63474403ec136bf2 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 27 Oct 2015 18:17:04 +0800 Subject: [PATCH 073/305] add secrets.yml.default --- .gitignore | 9 ++++++--- config/secrets.yml.default | 12 ++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 config/secrets.yml.default diff --git a/.gitignore b/.gitignore index 2b6ac398..9eded787 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,15 @@ # or operating system, you probably want to add a global ignore instead: # git config --global core.excludesfile ~/.gitignore_global +#assety stuff realtime/node_modules -config/database.yml -.env public/assets +#secrets +config/database.yml +config/secrets.yml +.env + # Ignore bundler config .bundle @@ -20,5 +24,4 @@ log/*.log tmp .DS_Store - .vagrant diff --git a/config/secrets.yml.default b/config/secrets.yml.default new file mode 100644 index 00000000..6859262d --- /dev/null +++ b/config/secrets.yml.default @@ -0,0 +1,12 @@ +defaults: &defaults + missing_map_png_url: '/assets/missing-map.png' + user_png_url: '/assets/user.png' + +development: + <<: *defaults + +test: + <<: *defaults + +production: + <<: *defaults From d8da2d93fd25111a3eeef166c8e0107b0b1f3657 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Tue, 27 Oct 2015 21:44:38 -0700 Subject: [PATCH 074/305] updated files to use amazon assets --- .gitignore | 2 + app/models/map.rb | 2 +- app/models/user.rb | 2 +- public/404.html | 4 +- test/fixtures/metacodes.yml | 94 ++++++++++++++++++------------------- 5 files changed, 53 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index 9eded787..c6358ead 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ log/*.log tmp .DS_Store +*/.DS_Store +.DS_Store? .vagrant diff --git a/app/models/map.rb b/app/models/map.rb index eab88814..75774df8 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -12,7 +12,7 @@ class Map < ActiveRecord::Base :thumb => ['188x126#', :png] #:full => ['940x630#', :png] }, - :default_url => ActionController::Base.helpers.asset_path('missing-map.png') + :default_url => 'https://s3.amazonaws.com/metamaps-assets/site/missing-map.png' # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :screenshot, :content_type => /\Aimage\/.*\Z/ diff --git a/app/models/user.rb b/app/models/user.rb index effa6ec5..f6b0d65a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,7 +35,7 @@ class User < ActiveRecord::Base :ninetysix => ['96x96#', :png], :onetwentyeight => ['128x128#', :png] }, - :default_url => ActionController::Base.helpers.asset_path('user.png') + :default_url => 'https://s3.amazonaws.com/metamaps-assets/site/user.png' # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :image, :content_type => /\Aimage\/.*\Z/ diff --git a/public/404.html b/public/404.html index 643a5ded..da435ea6 100644 --- a/public/404.html +++ b/public/404.html @@ -26,7 +26,7 @@ } body { - background: #d8d9da url(/assets/shattered_@2X.png); + background: #d8d9da url(https://s3.amazonaws.com/metamaps-assets/site/shattered_%402X.png); font-family: 'din-regular', helvetica, sans-serif; color: #424242; text-align: justify; @@ -61,7 +61,7 @@ border-radius: 225px; -webkit-border-radius: 225px; -moz-border-radius: 225px; - background: url(/assets/monkeyselfie.jpg) no-repeat; + background: url(https://s3.amazonaws.com/metamaps-assets/site/monkeyselfie.jpg) no-repeat; float: left; background-position:50% 20%; background-size: 100%; diff --git a/test/fixtures/metacodes.yml b/test/fixtures/metacodes.yml index 02ec0663..80780734 100644 --- a/test/fixtures/metacodes.yml +++ b/test/fixtures/metacodes.yml @@ -6,235 +6,235 @@ # one: name: Action - icon: /assets/icons/blueprint_96px/bp_action.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_action.png color: #BD6C85 two: name: Activity - icon: /assets/icons/blueprint_96px/bp_activity.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_activity.png color: #6EBF65 three: name: Catalyst - icon: /assets/icons/blueprint_96px/bp_catalyst.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_catalyst.png color: #EF8964 four: name: Closed - icon: /assets/icons/blueprint_96px/bp_closedissue.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_closedissue.png color: #ABB49F five: name: Process - icon: /assets/icons/blueprint_96px/bp_process.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_process.png color: #BDB25E six: name: Future Dev - icon: /assets/icons/blueprint_96px/bp_futuredev.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_futuredev.png color: #25A17F seven: name: Group - icon: /assets/icons/blueprint_96px/bp_group.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_group.png color: #7076BC eight: name: Implication - icon: /assets/icons/blueprint_96px/bp_implication.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_implication.png color: #83DECA nine: name: Insight - icon: /assets/icons/blueprint_96px/bp_insight.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_insight.png color: #B074AD ten: name: Intention - icon: /assets/icons/blueprint_96px/bp_intention.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_intention.png color: #BAEAFF eleven: name: Knowledge - icon: /assets/icons/blueprint_96px/bp_knowledge.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_knowledge.png color: #60ACF7 twelve: name: Location - icon: /assets/icons/blueprint_96px/bp_location.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_location.png color: #ABD9A7 thirteen: name: Need - icon: /assets/icons/blueprint_96px/bp_need.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_need.png color: #D2A7D4 fourteen: name: Open Issue - icon: /assets/icons/blueprint_96px/bp_openissue.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_openissue.png color: #9BBF71 fifteen: name: Opportunity - icon: /assets/icons/blueprint_96px/bp_opportunity.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_opportunity.png color: #889F64 sixteen: name: Person - icon: /assets/icons/blueprint_96px/bp_person.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_person.png color: #DE925F seventeen: name: Platform - icon: /assets/icons/blueprint_96px/bp_platform.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_platform.png color: #21C8FE eighteen: name: Problem - icon: /assets/icons/blueprint_96px/bp_problem.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_problem.png color: #99CFC4 nineteen: name: Resource - icon: /assets/icons/blueprint_96px/bp_resource.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_resource.png color: #C98C63 twenty: name: Role - icon: /assets/icons/blueprint_96px/bp_role.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_role.png color: #A8595D twenty-one: name: Task - icon: /assets/icons/blueprint_96px/bp_task.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_task.png color: #3397C4 twenty-two: name: Trajectory - icon: /assets/icons/blueprint_96px/bp_trajectory.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_trajectory.png color: #D3AA4C twenty-three: name: Argument - icon: /assets/icons/generics_96px/gen_argument.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_argument.png color: #7FAEFD twenty-four: name: Con - icon: /assets/icons/generics_96px/gen_con.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_con.png color: #CF7C74 twenty-five: name: Subject - icon: /assets/icons/generics_96px/gen_subject.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_subject.png color: #8293D8 twenty-six: name: Decision - icon: /assets/icons/generics_96px/gen_decision.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_decision.png color: #CCA866 twenty-seven: name: Event - icon: /assets/icons/generics_96px/gen_event.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_event.png color: #F5854B twenty-eight: name: Example - icon: /assets/icons/generics_96px/gen_example.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_example.png color: #618C61 twenty-nine: name: Experience - icon: /assets/icons/generics_96px/gen_experience.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_experience.png color: #BE995F thirty: name: Feedback - icon: /assets/icons/generics_96px/gen_feedback.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_feedback.png color: #54A19D thirty-one: name: Aim - icon: /assets/icons/generics_96px/gen_aim.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_aim.png color: #B0B0B0 thirty-two: name: Good Practice - icon: /assets/icons/generics_96px/gen_goodpractice.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_goodpractice.png color: #BD9E86 thirty-three: name: Idea - icon: /assets/icons/generics_96px/gen_idea.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_idea.png color: #C4BC5E thirty-four: name: List - icon: /assets/icons/generics_96px/gen_list.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_list.png color: #B7A499 thirty-five: name: Media - icon: /assets/icons/generics_96px/gen_media.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_media.png color: #6D94CC thirty-six: name: Metamap - icon: /assets/icons/generics_96px/gen_metamap.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_metamap.png color: #AEA9FD thirty-seven: name: Model - icon: /assets/icons/generics_96px/gen_model.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_model.png color: #B385BA thirty-eight: name: Note - icon: /assets/icons/generics_96px/gen_note.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_note.png color: #A389A1 thirty-nine: name: Perspective - icon: /assets/icons/generics_96px/gen_perspective.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_perspective.png color: #2EB6CC forty: name: Pro - icon: /assets/icons/generics_96px/gen_pro.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_pro.png color: #89B879 forty-one: name: Project - icon: /assets/icons/generics_96px/gen_project.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_project.png color: #85A050 forty-two: name: Question - icon: /assets/icons/generics_96px/gen_question.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_question.png color: #5CB3B3 forty-three: name: Reference - icon: /assets/icons/generics_96px/gen_reference.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_reference.png color: #A7A7A7 forty-four: name: Research - icon: /assets/icons/generics_96px/gen_research.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_research.png color: #CD8E89 forty-five: name: Status update - icon: /assets/icons/generics_96px/gen_status.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_status.png color: #EFA7C0 forty-six: name: Tool - icon: /assets/icons/generics_96px/gen_tool.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_tool.png color: #828282 forty-seven: name: Wildcard - icon: /assets/icons/generics_96px/gen_wildcard.png + icon: https://s3.amazonaws.com/metamaps-assets/metacodes/generics/96px/gen_wildcard.png color: #73C7DE From d99ed6b6271c702e8b574f98e99c3985e9fe318b Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Wed, 28 Oct 2015 14:24:52 +0800 Subject: [PATCH 075/305] migrate metacodes that started with /assets/icons to use amazonaws urls. Remove last migration I made --- ...20151025083043_remove_asset_paths_from_metacodes.rb | 8 -------- .../20151028061513_metacode_asset_path_update.rb | 10 ++++++++++ 2 files changed, 10 insertions(+), 8 deletions(-) delete mode 100644 db/migrate/20151025083043_remove_asset_paths_from_metacodes.rb create mode 100644 db/migrate/20151028061513_metacode_asset_path_update.rb diff --git a/db/migrate/20151025083043_remove_asset_paths_from_metacodes.rb b/db/migrate/20151025083043_remove_asset_paths_from_metacodes.rb deleted file mode 100644 index c5a5e842..00000000 --- a/db/migrate/20151025083043_remove_asset_paths_from_metacodes.rb +++ /dev/null @@ -1,8 +0,0 @@ -class RemoveAssetPathsFromMetacodes < ActiveRecord::Migration - def change - Metacode.all.each do |metacode| - metacode.icon = metacode.icon.gsub(/^\/assets\//, '') - metacode.save - end - end -end diff --git a/db/migrate/20151028061513_metacode_asset_path_update.rb b/db/migrate/20151028061513_metacode_asset_path_update.rb new file mode 100644 index 00000000..062e9d7e --- /dev/null +++ b/db/migrate/20151028061513_metacode_asset_path_update.rb @@ -0,0 +1,10 @@ +class MetacodeAssetPathUpdate < ActiveRecord::Migration + def change + Metacode.all.each do |metacode| + if metacode.icon.start_with?("/assets/icons/") + metacode.icon = metacode.icon.gsub(/^\/assets\/icons/, "https://s3.amazonaws.com/metamaps-assets/metacodes") + metacode.save + end + end + end +end From dd02129b0dd2f498522f1e6116a14150b8a5e911 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 29 Oct 2015 16:18:06 +0800 Subject: [PATCH 076/305] remove ruby from Gemfile in favour of .ruby-version --- Gemfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Gemfile b/Gemfile index b1333275..7b7da888 100644 --- a/Gemfile +++ b/Gemfile @@ -1,5 +1,4 @@ source 'https://rubygems.org' -ruby '2.1.3' gem 'rails', '4.2.4' From 570fa931b73f6386e1a7102bdbb8a8c3486d2f5b Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 29 Oct 2015 16:23:41 +0800 Subject: [PATCH 077/305] use secrets.yml to allow overriding config variables --- WindowsInstallation.md | 6 +++--- app/models/map.rb | 2 +- app/models/user.rb | 2 +- config/secrets.yml.default | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/WindowsInstallation.md b/WindowsInstallation.md index 42364b44..25268772 100644 --- a/WindowsInstallation.md +++ b/WindowsInstallation.md @@ -1,4 +1,4 @@ -First off, Metamaps runs on Ruby On Rails. Ruby 2.1.3 and Rails 3.2. You'll need to get Ruby and Rails installed on your computer if you don't already have it. Go to here for Ruby http://rubyinstaller.org/downloads/ +First off, Metamaps runs on Ruby On Rails. You'll need to get Ruby and Rails installed on your computer if you don't already have it. Go to here for Ruby http://rubyinstaller.org/downloads/ You'll also need GIT: http://git-scm.com/download/win @@ -7,7 +7,7 @@ It uses postgreSQL 9.2 as a database. You can install that for your computer fro Once you install those, open a 'command prompt with ruby'. to install rails - gem install rails -v 3.2 + gem install rails -v 4.2 also download node.js, which is also needed http://nodejs.org/download/ @@ -24,7 +24,7 @@ Install all the gems needed for Metamaps by running Setting up the database: -1) Copy /config/database.yml.default and rename the copy to /config/database.yml then edit database.yml with your text editor and set the password to whatever you chose when you set up the PostGres database. +1) Copy /config/database.yml.default and rename the copy to /config/database.yml then edit database.yml with your text editor and set the password to whatever you chose when you set up the PostGres database. Then do the same for /config/secrets.yml (the defaults should be OK for this file). 2) In a terminal: diff --git a/app/models/map.rb b/app/models/map.rb index 75774df8..59f10f9f 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -12,7 +12,7 @@ class Map < ActiveRecord::Base :thumb => ['188x126#', :png] #:full => ['940x630#', :png] }, - :default_url => 'https://s3.amazonaws.com/metamaps-assets/site/missing-map.png' + :default_url => Rails.secrets.missing_map_png_url # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :screenshot, :content_type => /\Aimage\/.*\Z/ diff --git a/app/models/user.rb b/app/models/user.rb index f6b0d65a..ecd09fcd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,7 +35,7 @@ class User < ActiveRecord::Base :ninetysix => ['96x96#', :png], :onetwentyeight => ['128x128#', :png] }, - :default_url => 'https://s3.amazonaws.com/metamaps-assets/site/user.png' + :default_url => Rails.secrets.user_png_url # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :image, :content_type => /\Aimage\/.*\Z/ diff --git a/config/secrets.yml.default b/config/secrets.yml.default index 6859262d..eb35a382 100644 --- a/config/secrets.yml.default +++ b/config/secrets.yml.default @@ -1,6 +1,6 @@ defaults: &defaults - missing_map_png_url: '/assets/missing-map.png' - user_png_url: '/assets/user.png' + missing_map_png_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map.png' + user_png_url: 'https://s3.amazonaws.com/metamaps-assets/site/user.png' development: <<: *defaults From ede4e7a509e1701ef5bd4d06b1f67d0808817250 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 30 Oct 2015 14:18:50 +0800 Subject: [PATCH 078/305] fix references to Rails.application.secrets --- app/models/map.rb | 2 +- app/models/user.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/map.rb b/app/models/map.rb index 59f10f9f..091c684b 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -12,7 +12,7 @@ class Map < ActiveRecord::Base :thumb => ['188x126#', :png] #:full => ['940x630#', :png] }, - :default_url => Rails.secrets.missing_map_png_url + :default_url => Rails.application.secrets.missing_map_png_url # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :screenshot, :content_type => /\Aimage\/.*\Z/ diff --git a/app/models/user.rb b/app/models/user.rb index ecd09fcd..8471f6b9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,7 +35,7 @@ class User < ActiveRecord::Base :ninetysix => ['96x96#', :png], :onetwentyeight => ['128x128#', :png] }, - :default_url => Rails.secrets.user_png_url + :default_url => Rails.application.secrets.user_png_url # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :image, :content_type => /\Aimage\/.*\Z/ From 10b58399a79edf295d588d4aa66c3e5bb2899f7a Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 30 Oct 2015 14:21:47 +0800 Subject: [PATCH 079/305] ok put ruby back in Gemfile' --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index 7b7da888..b1333275 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,5 @@ source 'https://rubygems.org' +ruby '2.1.3' gem 'rails', '4.2.4' From 3674aefb04880781076adae94529e08523ed2800 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 30 Oct 2015 14:30:24 +0800 Subject: [PATCH 080/305] remove secrets.yml stuff because it doesn't work with heroku --- app/models/map.rb | 2 +- app/models/user.rb | 2 +- config/secrets.yml.default | 12 ------------ 3 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 config/secrets.yml.default diff --git a/app/models/map.rb b/app/models/map.rb index 091c684b..e6a10605 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -12,7 +12,7 @@ class Map < ActiveRecord::Base :thumb => ['188x126#', :png] #:full => ['940x630#', :png] }, - :default_url => Rails.application.secrets.missing_map_png_url + :default_url => 'https://s3.amazonaws.com/metamaps-assets/site/missing-map.png' # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :screenshot, :content_type => /\Aimage\/.*\Z/ diff --git a/app/models/user.rb b/app/models/user.rb index 8471f6b9..f6b0d65a 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,7 +35,7 @@ class User < ActiveRecord::Base :ninetysix => ['96x96#', :png], :onetwentyeight => ['128x128#', :png] }, - :default_url => Rails.application.secrets.user_png_url + :default_url => 'https://s3.amazonaws.com/metamaps-assets/site/user.png' # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :image, :content_type => /\Aimage\/.*\Z/ diff --git a/config/secrets.yml.default b/config/secrets.yml.default deleted file mode 100644 index eb35a382..00000000 --- a/config/secrets.yml.default +++ /dev/null @@ -1,12 +0,0 @@ -defaults: &defaults - missing_map_png_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map.png' - user_png_url: 'https://s3.amazonaws.com/metamaps-assets/site/user.png' - -development: - <<: *defaults - -test: - <<: *defaults - -production: - <<: *defaults From 6df2f22080d411122ca7b22d72c013cf68345542 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 2 Nov 2015 00:05:57 +0800 Subject: [PATCH 081/305] update devise.rb with new changes --- config/initializers/devise.rb | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 2924b11e..d01678be 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,6 +1,13 @@ # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. Devise.setup do |config| + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` on Rails 4+ applications as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = '4d38a819bcea6314ffccb156a8e84b1b52c51ed446d11877c973791b3cd88449e9dbd7990cbc6e7f37d84702168ec36391467000c842ed5bed4f0b05df2b9507' + # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, # note that it will be overwritten if you use your own mailer class with default "from" parameter. @@ -121,6 +128,9 @@ Devise.setup do |config| # The time the user will be remembered without asking for credentials again. config.remember_for = 2.weeks + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + # If true, extends the user's remember period when remembered via cookie. # config.extend_remember_period = false @@ -142,9 +152,6 @@ Devise.setup do |config| # time the user will be asked for credentials again. Default is 30 minutes. # config.timeout_in = 30.minutes - # If true, expires auth token on session timeout. - # config.expire_auth_token_on_timeout = false - # ==> Configuration for :lockable # Defines which strategy will be used to lock an account. # :failed_attempts = Locks an account after a number of failed attempts to sign in. @@ -152,7 +159,7 @@ Devise.setup do |config| # config.lock_strategy = :failed_attempts # Defines which key will be used when locking and unlocking an account - # config.unlock_keys = [ :email ] + # config.unlock_keys = [:email] # Defines which strategy will be used to unlock an account. # :email = Sends an unlock link to the user email @@ -168,16 +175,23 @@ Devise.setup do |config| # Time interval to unlock the account if :time is enabled as unlock_strategy. # config.unlock_in = 1.hour + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + # ==> Configuration for :recoverable # # Defines which key will be used when recovering the password for an account - # config.reset_password_keys = [ :email ] + # config.reset_password_keys = [:email] # Time interval you can reset your password with a reset password key. # Don't put a too small interval or your users won't have the time to # change their passwords. config.reset_password_within = 24.hours + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + # ==> Configuration for :encryptable # Allow you to use another encryption algorithm besides bcrypt (default). You can use # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, @@ -186,10 +200,6 @@ Devise.setup do |config| # REST_AUTH_SITE_KEY to pepper) # config.encryptor = :sha512 - # ==> Configuration for :token_authenticatable - # Defines name of the authentication token params key - # config.token_authentication_key = :auth_token - # ==> Scopes configuration # Turn scoped views on. Before rendering "sessions/new", it will first check for # "users/sessions/new". It's turned off by default because it's slower if you @@ -237,12 +247,12 @@ Devise.setup do |config| # is mountable, there are some extra configurations to be taken into account. # The following options are available, assuming the engine is mounted as: # - # mount MyEngine, at: "/my_engine" + # mount MyEngine, at: '/my_engine' # # The router that invoked `devise_for`, in the example above, would be: # config.router_name = :my_engine # - # When using omniauth, Devise cannot automatically set Omniauth path, + # When using OmniAuth, Devise cannot automatically set OmniAuth path, # so you need to do it manually. For the users scope, it would be: - # config.omniauth_path_prefix = "/my_engine/users/auth" + # config.omniauth_path_prefix = '/my_engine/users/auth' end From 506f93e1440042d8bdbfceec3b4abcf1ec5a01fb Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 2 Nov 2015 00:06:12 +0800 Subject: [PATCH 082/305] Gemfile.lock --- Gemfile.lock | 3 --- 1 file changed, 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index ca22d0b8..f5fa5a3f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -234,6 +234,3 @@ DEPENDENCIES sass-rails uglifier uservoice-ruby - -BUNDLED WITH - 1.10.6 From 20e698f69df861d6efbfc347bb453e169e6902a7 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 2 Nov 2015 00:07:52 +0800 Subject: [PATCH 083/305] devise locale text --- config/locales/devise.en.yml | 39 ++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml index d01f375c..26a10f29 100644 --- a/config/locales/devise.en.yml +++ b/config/locales/devise.en.yml @@ -3,49 +3,50 @@ en: devise: confirmations: - confirmed: "Your account was successfully confirmed. You are now signed in." - send_instructions: "You will receive an email with instructions about how to confirm your account in a few minutes." - send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions about how to confirm your account in a few minutes." + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." failure: already_authenticated: "You are already signed in." - inactive: "Your account was not activated yet." - invalid: "Invalid email or password." - invalid_token: "Invalid authentication token." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys} or password." locked: "Your account is locked." - not_found_in_database: "Invalid email or password." - timeout: "Your session expired, please sign in again to continue." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please sign in again to continue." unauthenticated: "You need to sign in or sign up before continuing." - unconfirmed: "You have to confirm your account before continuing." + unconfirmed: "You have to confirm your email address before continuing." mailer: confirmation_instructions: subject: "Confirmation instructions" reset_password_instructions: subject: "Reset password instructions" unlock_instructions: - subject: "Unlock Instructions" + subject: "Unlock instructions" omniauth_callbacks: failure: "Could not authenticate you from %{kind} because \"%{reason}\"." success: "Successfully authenticated from %{kind} account." passwords: no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." - send_instructions: "You will receive an email with instructions about how to reset your password in a few minutes." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." - updated: "Your password was changed successfully. You are now signed in." - updated_not_active: "Your password was changed successfully." + updated: "Your password has been changed successfully. You are now signed in." + updated_not_active: "Your password has been changed successfully." registrations: - destroyed: "Bye! Your account was successfully cancelled. We hope to see you again soon." + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." signed_up: "Welcome! You have signed up successfully." signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." - signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please open the link to activate your account." - update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm link to finalize confirming your new email address." - updated: "You updated your account successfully." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." + updated: "Your account has been updated successfully." sessions: signed_in: "Signed in successfully." signed_out: "Signed out successfully." + already_signed_out: "Signed out successfully." unlocks: - send_instructions: "You will receive an email with instructions about how to unlock your account in a few minutes." - send_paranoid_instructions: "If your account exists, you will receive an email with instructions about how to unlock it in a few minutes." + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." unlocked: "Your account has been unlocked successfully. Please sign in to continue." errors: messages: From dfaadc691e1f968e758736886ac444df8d6872cc Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 3 Nov 2015 19:10:21 +0800 Subject: [PATCH 084/305] update WindowsInstallation.md --- WindowsInstallation.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/WindowsInstallation.md b/WindowsInstallation.md index 25268772..f4ed3768 100644 --- a/WindowsInstallation.md +++ b/WindowsInstallation.md @@ -1,38 +1,38 @@ -First off, Metamaps runs on Ruby On Rails. You'll need to get Ruby and Rails installed on your computer if you don't already have it. Go to here for Ruby http://rubyinstaller.org/downloads/ +Before you begin, you'll need to install a number of software packages: -You'll also need GIT: http://git-scm.com/download/win +Ruby: http://rubyinstaller.org/downloads +Git: http://git-scm.com/download/win +PostgreSQL 9.2: http://www.enterprisedb.com/products-services-training/pgdownload +nodejs: http://nodejs.org/download -It uses postgreSQL 9.2 as a database. You can install that for your computer from here: http://www.enterprisedb.com/products-services-training/pgdownload . During installation you can choose whatever database password you like. Make sure to note it down! +During the installation of the PostgreSQL database, you'll need to choose a database password. Anything is fine, just note what you choose somewhere. -Once you install those, open a 'command prompt with ruby'. +Once you are ready, create a new folder to hold this and any other git repositories. As an example, let's pretend you've chose C:\git, and made that folder writable by your user account. -to install rails +Open a command prompt ("cmd.exe"), and navigate to the folder you chose. Then use the gem command (which is part of Ruby) to install Ruby on Rails. + + cd \git gem install rails -v 4.2 - -also download node.js, which is also needed http://nodejs.org/download/ -Navigate to the folder that you want to download the metamaps files to and run the following: (use your forked git repository address if it's different than this repo. You will also need to go to your Github account settings and add the SSH key that was placed in your clipboard earlier) +Now you are ready to clone the Metamaps git repository: git clone https://github.com/metamaps/metamaps_gen002.git --branch develop cd metamaps_gen002 - -Now you're in the main directory. - -Install all the gems needed for Metamaps by running - bundle install -Setting up the database: +The third `bundle install` command downloads and installs the rubygem dependencies of Metamaps. + +At this point you should be in C:\git\metamaps_gen002, or whatever equivalent directory you've chosen. The next step is to set up your database configuration. From the metamaps_gen002 directory, run -1) Copy /config/database.yml.default and rename the copy to /config/database.yml then edit database.yml with your text editor and set the password to whatever you chose when you set up the PostGres database. Then do the same for /config/secrets.yml (the defaults should be OK for this file). - -2) In a terminal: + start config + +This command will open a Windows Explorer window of the "config" directory of Metamaps. Copy database.yml.default, and rename the copy to database.yml. Edit the file and set the password to be whatever you set up with postgres earlier. Once you're done, then move back into the command prompt. The next few commands will fail unless database.yml is correctly configured and Postgres is running. rake db:create rake db:schema:load rake db:fixtures:load -Running the server: +And you're set up! At this point, you should be able to run the server at any time with only one command; you don't need to repeat any of the previous steps again. The command to run the server is: rails s From f4456d06ef682553ea67f2680ea4d02f80cc0b7c Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 3 Nov 2015 20:56:50 +0800 Subject: [PATCH 085/305] fix devise integration for rails 4 --- app/controllers/registrations_controller.rb | 10 ---------- app/controllers/users/registrations_controller.rb | 13 +++++++++++++ app/controllers/users_controller.rb | 9 ++++----- config/application.rb | 2 +- 4 files changed, 18 insertions(+), 16 deletions(-) delete mode 100644 app/controllers/registrations_controller.rb diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb deleted file mode 100644 index 5fff2f1c..00000000 --- a/app/controllers/registrations_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -class Users::RegistrationsController < Devise::RegistrationsController - protected - def after_sign_up_path_for(resource) - signed_in_root_path(resource) - end - - def after_update_path_for(resource) - signed_in_root_path(resource) - end -end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 5fff2f1c..c77edb50 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -1,4 +1,7 @@ class Users::RegistrationsController < Devise::RegistrationsController + before_filter :configure_sign_up_params, only: [:create] + before_filter :configure_account_update_params, only: [:update] + protected def after_sign_up_path_for(resource) signed_in_root_path(resource) @@ -7,4 +10,14 @@ class Users::RegistrationsController < Devise::RegistrationsController def after_update_path_for(resource) signed_in_root_path(resource) end + + private + def configure_sign_up_params + devise_parameter_sanitizer.for(:sign_up) << [:name, :joinedwithcode] + end + + def configure_account_update_params + puts devise_parameter_sanitizer_for(:account_update) + devise_parameter_sanitizer.for(:account_update) << [:image] + end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 52996f49..683e6200 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -22,9 +22,9 @@ class UsersController < ApplicationController def update @user = current_user - if params[:user][:password] == "" && params[:user][:password_confirmation] == "" + if user_params[:password] == "" && user_params[:password_confirmation] == "" # not trying to change the password - if @user.update_attributes(params[:user]) + if @user.update_attributes(user_params.except(:password, :password_confirmation)) if params[:remove_image] == "1" @user.image = nil end @@ -43,7 +43,7 @@ class UsersController < ApplicationController # trying to change the password correct_pass = @user.valid_password?(params[:current_password]) - if correct_pass && @user.update_attributes(params[:user]) + if correct_pass && @user.update_attributes(user_params) if params[:remove_image] == "1" @user.image = nil end @@ -101,8 +101,7 @@ class UsersController < ApplicationController private def user_params - params.require(:user).permit(:name, :email, :image, :password, - :password_confirmation, :code, :joinedwithcode, :remember_me) + params.require(:user).permit(:name, :email, :image, :password, :password_confirmation) end end diff --git a/config/application.rb b/config/application.rb index f9e0a87d..6bcfbe27 100644 --- a/config/application.rb +++ b/config/application.rb @@ -2,7 +2,7 @@ require File.expand_path('../boot', __FILE__) require 'rails/all' -Bundler.require(:default, Rails.env) +Bundler.require(*Rails.groups) module Metamaps class Application < Rails::Application From 3e03e6484592f31d4625d52d046752ab18261200 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 3 Nov 2015 21:02:54 +0800 Subject: [PATCH 086/305] css fixes for heroku --- ...ery.mCustomScrollbar.css => jquery.mCustomScrollbar.css.erb} | 1 - app/assets/stylesheets/uservoice.css.erb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) rename app/assets/stylesheets/{jquery.mCustomScrollbar.css => jquery.mCustomScrollbar.css.erb} (96%) diff --git a/app/assets/stylesheets/jquery.mCustomScrollbar.css b/app/assets/stylesheets/jquery.mCustomScrollbar.css.erb similarity index 96% rename from app/assets/stylesheets/jquery.mCustomScrollbar.css rename to app/assets/stylesheets/jquery.mCustomScrollbar.css.erb index f799cdc4..e8588f35 100644 --- a/app/assets/stylesheets/jquery.mCustomScrollbar.css +++ b/app/assets/stylesheets/jquery.mCustomScrollbar.css.erb @@ -162,7 +162,6 @@ .mCSB_scrollTools .mCSB_buttonDown, .mCSB_scrollTools .mCSB_buttonLeft, .mCSB_scrollTools .mCSB_buttonRight{ - background-image:url(<%= asset_data_uri('mCSB_buttons.png') %>); background-repeat:no-repeat; opacity:0.4; filter:"alpha(opacity=40)"; -ms-filter:"alpha(opacity=40)"; /* old ie */ diff --git a/app/assets/stylesheets/uservoice.css.erb b/app/assets/stylesheets/uservoice.css.erb index df15d675..f633c367 100644 --- a/app/assets/stylesheets/uservoice.css.erb +++ b/app/assets/stylesheets/uservoice.css.erb @@ -6,7 +6,7 @@ } div.uv-icon.uv-bottom-left { - background-image:url(<%= asset_data_uri 'feedback_sprite.png' %>); + background-image: url(<%= asset_data_uri 'feedback_sprite.png' %>); background-repeat: no-repeat; color:#FFFFFF; cursor:pointer; From 75700f06a92c706fa0173e10f52c62682ca62a8e Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 3 Nov 2015 22:22:53 +0800 Subject: [PATCH 087/305] make invite link use REQUEST_URI --- app/controllers/application_controller.rb | 11 +++++++++-- app/views/layouts/_lightboxes.html.erb | 5 ++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0a3d12ab..d275eaea 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,7 @@ class ApplicationController < ActionController::Base protect_from_forgery + + before_filter :get_invite_link # this is for global login include ContentHelper @@ -47,7 +49,6 @@ private current_user end - def authenticated? current_user end @@ -55,5 +56,11 @@ private def admin? current_user && current_user.admin end - + + def get_invite_link + unsafe_uri = request.env["REQUEST_URI"] + valid_url = /^https?:\/\/([\w\.-]+)(:\d{1,5})?\/?$/ + safe_uri = (unsafe_uri.match(valid_url)) ? unsafe_uri : "http://metamaps.cc/" + @invite_link = "#{safe_uri}join?code=#{current_user.code}" + end end diff --git a/app/views/layouts/_lightboxes.html.erb b/app/views/layouts/_lightboxes.html.erb index e8b688d5..46ab2edf 100644 --- a/app/views/layouts/_lightboxes.html.erb +++ b/app/views/layouts/_lightboxes.html.erb @@ -231,9 +231,8 @@

        As a valued beta tester, you have the ability to invite your peers, colleagues and collaborators onto the platform.

        Below is a personal invite link containing your unique access code, which can be used multiple times.

        - <% mapper = current_user %> -

        http://metamaps.cc/join?code=<%= mapper.code %>

        - +

        <%= @invite_link %> +

        From 1874c67a6697d38799ea96b6253b82f3d16a8282 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Wed, 4 Nov 2015 11:40:52 +0800 Subject: [PATCH 088/305] reorganize metamaps-qa-steps a bit --- doc/metamaps-qa-steps.md | 39 +++++++++++++++++++++++++++++++++++++++ metamaps-qa-steps.txt | 24 ------------------------ 2 files changed, 39 insertions(+), 24 deletions(-) create mode 100644 doc/metamaps-qa-steps.md delete mode 100644 metamaps-qa-steps.txt diff --git a/doc/metamaps-qa-steps.md b/doc/metamaps-qa-steps.md new file mode 100644 index 00000000..a3f136ca --- /dev/null +++ b/doc/metamaps-qa-steps.md @@ -0,0 +1,39 @@ +# Metamaps Tests + +Run these tests to be reasonably sure that your code changes haven't broken anything. + +### Users & Accounts + + - Create an account using your join code + - Log in to the interface + - Check your user's "generation" + - Edit your profile picture, email, name, and password + - Remove your profile picture + +### Maps, Topics, Synapses, and Permissions + + - Create three maps: private, public, and another public + - Change the last map's permissions to commons + - Change a map's name + - Create a topic on map #1 + - Verify (in a private window or another browser) that the second user can't acccess map #1 + - Create a topic on map #2 + - Verify that the second user **can't** edit map #2 + - Create a topic on map #3 + - Verify that the second user **can** edit map #3 + - Pull a topic from map #1 to map #3 + - Create a private topic on map #1 + - Verify that the private topic can be pulled from map #1 by the same user + - Verify that the private topic can't be pulled from map #1 by another user + +### Mappings + + - Add a number of topics to one of your maps. Reload to see if they are still there. + - Add a number of synapses to one of your maps. Reload to see if they are still there. + - Rearrange one of your maps and save the layout. Reload to see if the layout is preserved. + +### Misc + + - Login as admin. Change metacode sets. + - Set the screenshot for one of your maps, and verify the index of maps is updated. + - Open two browsers on map #3 and verify that realtime editing works (you'll need to be running the realtime server for this to work). diff --git a/metamaps-qa-steps.txt b/metamaps-qa-steps.txt deleted file mode 100644 index 36ea193a..00000000 --- a/metamaps-qa-steps.txt +++ /dev/null @@ -1,24 +0,0 @@ -Metamaps Test Suite - -1) Log in to the interface -2) Create an account using your join code -3) Check your user's "generation" -4) Create three maps: private, public, and another public -5) Change the last map's permissions to commons -6) Change a map's name -7) Create a topic on map #1 -8) Verify (in a private window or another browser) that the second user can't acccess map #1 -9) Create a topic on map #2 -10) Verify that the second user can't edit map #2 -11) Create a topic on map #3 -12) Verify that the second can edit map #3 -13) Pull a topic from map #1 to map #3 -14) Create a private topic on map #1 -15) Verify that the private topic can be pulled from map #1 by the same user -16) Verify that the private topic can't be pulled from map #1 by another user -17) Login as admin. Change metacode sets. -18) Add a number of topics to one of your maps. Reload to see if they are still there. -19) Add a number of synapses to one of your maps. Reload to see if they are still there. -20) Rearrange one of your maps. Reload to see if the layout is preserved. -21) Set the screenshot for one of your maps, and verify the index of maps is updated. -22) Open two browsers on map #3 and verify that realtime editing works. From f8814c060f547fa65493ff594a2b866be27c794d Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Wed, 4 Nov 2015 11:41:04 +0800 Subject: [PATCH 089/305] reorganize documentation --- README.md | 6 +++--- CHANGELOG.md => doc/CHANGELOG.md | 0 CONTRIBUTING.md => doc/CONTRIBUTING.md | 0 MacInstallation.md => doc/MacInstallation.md | 0 doc/README_FOR_APP | 2 -- UbuntuInstallation.md => doc/UbuntuInstallation.md | 0 WindowsInstallation.md => doc/WindowsInstallation.md | 0 7 files changed, 3 insertions(+), 5 deletions(-) rename CHANGELOG.md => doc/CHANGELOG.md (100%) rename CONTRIBUTING.md => doc/CONTRIBUTING.md (100%) rename MacInstallation.md => doc/MacInstallation.md (100%) delete mode 100644 doc/README_FOR_APP rename UbuntuInstallation.md => doc/UbuntuInstallation.md (100%) rename WindowsInstallation.md => doc/WindowsInstallation.md (100%) diff --git a/README.md b/README.md index 1e23d33b..8b0a9beb 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,6 @@ Copyright (c) 2015 Connor Turland [site-beta]: http://metamaps.cc [community]: https://plus.google.com/u/0/communities/115060009262157699234 [license]: https://github.com/metamaps/metamaps_gen002/blob/master/LICENSE -[contributing]: https://github.com/metamaps/metamaps_gen002/blob/master/CONTRIBUTING.md -[contributing-issues]: https://github.com/metamaps/metamaps_gen002/blob/master/CONTRIBUTING.md#reporting-bugs-and-other-issues -[windows-installation]: https://github.com/metamaps/metamaps_gen002/blob/master/WindowsInstallation.md +[contributing]: https://github.com/metamaps/metamaps_gen002/blob/master/doc/CONTRIBUTING.md +[contributing-issues]: https://github.com/metamaps/metamaps_gen002/blob/master/doc/CONTRIBUTING.md#reporting-bugs-and-other-issues +[windows-installation]: https://github.com/metamaps/metamaps_gen002/blob/master/doc/WindowsInstallation.md diff --git a/CHANGELOG.md b/doc/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to doc/CHANGELOG.md diff --git a/CONTRIBUTING.md b/doc/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to doc/CONTRIBUTING.md diff --git a/MacInstallation.md b/doc/MacInstallation.md similarity index 100% rename from MacInstallation.md rename to doc/MacInstallation.md diff --git a/doc/README_FOR_APP b/doc/README_FOR_APP deleted file mode 100644 index fe41f5cc..00000000 --- a/doc/README_FOR_APP +++ /dev/null @@ -1,2 +0,0 @@ -Use this README file to introduce your application and point to useful places in the API for learning more. -Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. diff --git a/UbuntuInstallation.md b/doc/UbuntuInstallation.md similarity index 100% rename from UbuntuInstallation.md rename to doc/UbuntuInstallation.md diff --git a/WindowsInstallation.md b/doc/WindowsInstallation.md similarity index 100% rename from WindowsInstallation.md rename to doc/WindowsInstallation.md From 87389a88ed5889d62ea9b1ee50d5e6ea2fe2e0ac Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Wed, 4 Nov 2015 00:14:14 -0500 Subject: [PATCH 090/305] current_user can be nil --- app/controllers/application_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d275eaea..4278637f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -61,6 +61,6 @@ private unsafe_uri = request.env["REQUEST_URI"] valid_url = /^https?:\/\/([\w\.-]+)(:\d{1,5})?\/?$/ safe_uri = (unsafe_uri.match(valid_url)) ? unsafe_uri : "http://metamaps.cc/" - @invite_link = "#{safe_uri}join?code=#{current_user.code}" + @invite_link = "#{safe_uri}join" + (current_user ? "?code=#{current_user.code}" : "") end end From 520fe095bda5974c7347b2ca96cda01cdfcc03b4 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Wed, 4 Nov 2015 17:22:46 +0800 Subject: [PATCH 091/305] fix #465, metacode sort problem --- app/helpers/application_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index fa7876f5..3db990db 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -9,7 +9,7 @@ module ApplicationHelper @m = user.settings.metacodes set = get_metacodeset if set - @metacodes = set.metacodes + @metacodes = set.metacodes.to_a else @metacodes = Metacode.where(id: @m).to_a end From 67d4a2aa34eb29a2049dea6bbb5bda028d8f654d Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 6 Nov 2015 17:08:42 +0800 Subject: [PATCH 092/305] sandi metz function simplification --- app/controllers/synapses_controller.rb | 3 +- app/controllers/users_controller.rb | 2 - app/helpers/users_helper.rb | 21 ++-------- app/models/user.rb | 39 ++++++++++++------- ...151028061513_metacode_asset_path_update.rb | 3 +- 5 files changed, 30 insertions(+), 38 deletions(-) diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index dc36ff28..2c3c08d0 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -50,8 +50,7 @@ class SynapsesController < ApplicationController # DELETE synapses/:id def destroy - @current = current_user - @synapse = Synapse.find(params[:id]).authorize_to_delete(@current) + @synapse = Synapse.find(params[:id]).authorize_to_delete(current_user) @synapse.delete if @synapse respond_to do |format| diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 683e6200..bb645614 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,5 +1,4 @@ class UsersController < ApplicationController - before_filter :require_user, only: [:edit, :update, :updatemetacodes] respond_to :html, :json @@ -14,7 +13,6 @@ class UsersController < ApplicationController # GET /users/:id/edit def edit @user = current_user - respond_with(@user) end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 977e5709..3c08494e 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -1,22 +1,9 @@ module UsersHelper - - ## this one is for building our custom JSON autocomplete format for typeahead + # build custom json autocomplete for typeahead def autocomplete_user_array_json(users) - temp = [] - users.each do |u| - user = {} - user['id'] = u.id - user['label'] = u.name - user['value'] = u.name - user['profile'] = u.image.url(:sixtyfour) - user['mapCount'] = u.maps.count - user['generation'] = u.generation - user['created_at'] = u.created_at.strftime("%m/%d/%Y") - user['rtype'] = "mapper" - - temp.push user + json_users = [] + users.each do |user| + json_users.push user.as_json_for_autocomplete end - return temp end - end diff --git a/app/models/user.rb b/app/models/user.rb index f6b0d65a..a0966405 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -40,6 +40,7 @@ class User < ActiveRecord::Base # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :image, :content_type => /\Aimage\/.*\Z/ + # override default as_json def as_json(options={}) { :id => self.id, :name => self.name, @@ -47,27 +48,36 @@ class User < ActiveRecord::Base :admin => self.admin } end + + def as_json_for_autocomplete + user = {} + user['id'] = u.id + user['label'] = u.name + user['value'] = u.name + user['profile'] = u.image.url(:sixtyfour) + user['mapCount'] = u.maps.count + user['generation'] = u.generation + user['created_at'] = u.created_at.strftime("%m/%d/%Y") + user['rtype'] = "mapper" + end + #generate a random 8 letter/digit code that they can use to invite people def generate_code - #generate a random 8 letter/digit code that they can use to invite people self.code = rand(36**8).to_s(36) - $codes.push(self.code) - self.generation = self.get_generation end def get_generation - if self.joinedwithcode == self.code - # if your joinedwithcode equals your code you must be GEN 0 - gen = 0 - elsif self.generation - # if your generation has already been calculated then just return that value - gen = self.generation + calculate_generation() if generation.nil? + generation + end + + def calculate_generation + if code == joinedwithcode + update(generation: 0) else - # if your generation hasn't been calculated, base it off the - # generation of the person whose code you joined with + 1 - gen = User.find_by_code(self.joinedwithcode).get_generation + 1 + update(generation: User.find_by_code(joinedwithcode) + 1 end end @@ -75,13 +85,12 @@ class User < ActiveRecord::Base # make sure we always return a UserPreference instance if read_attribute(:settings).nil? write_attribute :settings, UserPreference.new - read_attribute :settings - else - read_attribute :settings end + read_attribute :settings end def settings=(val) write_attribute :settings, val end + end diff --git a/db/migrate/20151028061513_metacode_asset_path_update.rb b/db/migrate/20151028061513_metacode_asset_path_update.rb index 062e9d7e..be3607d1 100644 --- a/db/migrate/20151028061513_metacode_asset_path_update.rb +++ b/db/migrate/20151028061513_metacode_asset_path_update.rb @@ -2,8 +2,7 @@ class MetacodeAssetPathUpdate < ActiveRecord::Migration def change Metacode.all.each do |metacode| if metacode.icon.start_with?("/assets/icons/") - metacode.icon = metacode.icon.gsub(/^\/assets\/icons/, "https://s3.amazonaws.com/metamaps-assets/metacodes") - metacode.save + metacode.update(icon: metacode.icon.gsub(/^\/assets\/icons/, "https://s3.amazonaws.com/metamaps-assets/metacodes")) end end end From dcbe24bb7e977da065b2c507ef38a3db7374ffc0 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Wed, 4 Nov 2015 17:52:58 -0500 Subject: [PATCH 093/305] getting uploads working followed what it said to do here: http://stackoverflow.com/questions/28374401/nameerror-uninitialized-constant-paperclipstorages3aws --- Gemfile | 2 +- Gemfile.lock | 18 +++++++++--------- config/routes.rb | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index b1333275..ed6a6863 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,7 @@ gem 'uservoice-ruby' gem 'dotenv' gem 'paperclip' -gem 'aws-sdk' +gem 'aws-sdk', '< 2.0' gem 'jquery-rails' gem 'jquery-ui-rails' diff --git a/Gemfile.lock b/Gemfile.lock index f5fa5a3f..b9ba0c6b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -37,12 +37,11 @@ GEM thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) arel (6.0.3) - aws-sdk (2.1.19) - aws-sdk-resources (= 2.1.19) - aws-sdk-core (2.1.19) - jmespath (~> 1.0) - aws-sdk-resources (2.1.19) - aws-sdk-core (= 2.1.19) + aws-sdk (1.66.0) + aws-sdk-v1 (= 1.66.0) + aws-sdk-v1 (1.66.0) + json (~> 1.4) + nokogiri (>= 1.4.4) bcrypt (3.1.10) best_in_place (3.0.3) actionpack (>= 3.2) @@ -92,8 +91,6 @@ GEM jbuilder (2.3.1) activesupport (>= 3.0.0, < 5) multi_json (~> 1.2) - jmespath (1.0.2) - multi_json (~> 1.0) jquery-rails (4.0.5) rails-dom-testing (~> 1.0) railties (>= 4.2.0) @@ -207,7 +204,7 @@ PLATFORMS ruby DEPENDENCIES - aws-sdk + aws-sdk (< 2.0) best_in_place better_errors binding_of_caller @@ -234,3 +231,6 @@ DEPENDENCIES sass-rails uglifier uservoice-ruby + +BUNDLED WITH + 1.10.6 diff --git a/config/routes.rb b/config/routes.rb index e80b837a..a3ab6e3a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,7 +26,7 @@ Metamaps::Application.routes.draw do get 'explore/mapper/:id', to: 'maps#index', as: :usermaps resources :maps, except: [:new, :edit] get 'maps/:id/contains', to: 'maps#contains', as: :contains - get 'maps/:id/upload_screenshot', to: 'maps#screenshot', as: :screenshot + post 'maps/:id/upload_screenshot', to: 'maps#screenshot', as: :screenshot devise_for :users, controllers: { registrations: 'users/registrations', passwords: 'users/passwords', sessions: 'devise/sessions' }, :skip => :sessions From 1b60927641a0e4a22db1df4be7c7e652f4ce52e3 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Wed, 4 Nov 2015 18:09:12 -0500 Subject: [PATCH 094/305] had to update because jquery ui version changed --- app/assets/javascripts/src/Metamaps.js.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index 84fd38bc..d15172c9 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -656,7 +656,7 @@ Metamaps.Create = { self.newSelectedMetacodeNames = self.selectedMetacodeNames.slice(0); self.newSelectedMetacodes = self.selectedMetacodes.slice(0); } - $('#metacodeSwitchTabs').tabs("select", self.selectedMetacodeSetIndex); + $('#metacodeSwitchTabs').tabs("option", "active", self.selectedMetacodeSetIndex); $('#topic_name').focus(); }, newTopic: { From 5f14601c745f9831c370fde5b1f64b63cc9ac6e8 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Wed, 4 Nov 2015 18:09:44 -0500 Subject: [PATCH 095/305] styling of the vertical tab selectors needed improvement --- app/assets/stylesheets/application.css.erb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/application.css.erb b/app/assets/stylesheets/application.css.erb index b671dd80..6c036de1 100644 --- a/app/assets/stylesheets/application.css.erb +++ b/app/assets/stylesheets/application.css.erb @@ -2146,6 +2146,7 @@ and it won't be important on password protected instances */ display: block; width: 100%; padding: 4px 0 !important; + outline: none; } .ui-tabs-vertical .ui-tabs-panel { padding: 0 !important; @@ -2182,6 +2183,7 @@ and it won't be important on password protected instances */ } #metacodeSwitchTabs li.ui-state-active a { color: #00BCD4; + cursor: pointer; } .metacodeSwitchTab { max-height: 300px; From 5b0e7ffcde0c6d1dbf43fa49c31bfeccdfff6387 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Wed, 4 Nov 2015 18:10:20 -0500 Subject: [PATCH 096/305] switching metacode sets was being caught by Metamaps.Router.intercept --- app/views/shared/_switchmetacodes.html.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/shared/_switchmetacodes.html.erb b/app/views/shared/_switchmetacodes.html.erb index 067d4d6b..e0fcd036 100644 --- a/app/views/shared/_switchmetacodes.html.erb +++ b/app/views/shared/_switchmetacodes.html.erb @@ -19,9 +19,9 @@
        <% allMetacodeSets.each_with_index do |m, localindex| %>
        Date: Wed, 4 Nov 2015 19:50:32 -0500 Subject: [PATCH 097/305] fix cannot set readonly property highlight --- app/assets/javascripts/src/Metamaps.GlobalUI.js.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb index 5c575e88..0290fee1 100644 --- a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb +++ b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb @@ -459,7 +459,7 @@ Metamaps.GlobalUI.Search = { $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ width: '0' }, 300, function () { - $('.sidebarSearchField').typeahead('setQuery', ''); + $('.sidebarSearchField').typeahead('val', ''); $('.sidebarSearchField').blur(); self.changing = false; self.isOpen = false; @@ -635,7 +635,7 @@ Metamaps.GlobalUI.Search = { $('.limitToMe').unbind().bind("change", function (e) { // set the value of the search equal to itself to retrigger the autocomplete event self.isOpen = false; - $('.sidebarSearchField').typeahead('setQuery', $('.sidebarSearchField').val()); + $('.sidebarSearchField').typeahead('val', $('.sidebarSearchField').val()); setTimeout(function () { self.isOpen = true; }, 2000); From 4139c2c84adcebf9c3f24556c7c3188c07269109 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 7 Nov 2015 01:34:08 +0800 Subject: [PATCH 098/305] syntax error --- app/models/user.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index a0966405..cb06f646 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -77,7 +77,7 @@ class User < ActiveRecord::Base if code == joinedwithcode update(generation: 0) else - update(generation: User.find_by_code(joinedwithcode) + 1 + update(generation: User.find_by_code(joinedwithcode) + 1) end end From 43624caf8800d89381084b0887fa7e9a9692be6e Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 8 Nov 2015 22:53:34 +0800 Subject: [PATCH 099/305] get autocomplete working again with new typeahead.js, but not the CSS --- .../javascripts/src/Metamaps.GlobalUI.js.erb | 160 ++++++++++-------- 1 file changed, 87 insertions(+), 73 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb index 0290fee1..1e48af33 100644 --- a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb +++ b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb @@ -477,95 +477,109 @@ Metamaps.GlobalUI.Search = { var topics = { name: 'topics', limit: 9999, - dupChecker: function (datum1, datum2) { - return false; - }, - template: $('#topicSearchTemplate').html(), - remote: { - url: '/search/topics?term=%QUERY', - replace: function () { - var q = '/search/topics?term=' + $('.sidebarSearchField').val(); - if (Metamaps.Active.Mapper && $("#limitTopicsToMe").is(':checked')) { - q += "&user=" + Metamaps.Active.Mapper.id.toString(); - } - return q; + + display: function(s) { return s.label; }, + templates: { + notFound: function(s) { + return Hogan.compile($('#topicSearchTemplate').html()).render({ + value: "No results", + label: "No results", + typeImageURL: "<%= asset_path('icons/wildcard.png') %>", + rtype: "noresult" + }); + }, + header: topicheader, + suggestion: function(s) { + return Hogan.compile($('#topicSearchTemplate').html()).render(s); }, - filter: function (dataset) { - if (dataset.length == 0) { - dataset.push({ - value: "No results", - label: "No results", - typeImageURL: "<%= asset_path('icons/wildcard.png') %>", - rtype: "noresult" - }); - } - return dataset; - } }, - engine: Hogan, - header: topicheader + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/topics', + prepare: function(query, settings) { + settings.url += '?term=' + $('.sidebarSearchField').val(); + if (Metamaps.Active.Mapper && $("#limitTopicsToMe").is(':checked')) { + settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); + } + return settings; + }, + }, + }), }; var maps = { name: 'maps', limit: 9999, - dupChecker: function (datum1, datum2) { - return false; - }, - template: $('#mapSearchTemplate').html(), - remote: { - url: '/search/maps?term=%QUERY', - replace: function () { - var q = '/search/maps?term=' + $('.sidebarSearchField').val(); - if (Metamaps.Active.Mapper && $("#limitMapsToMe").is(':checked')) { - q += "&user=" + Metamaps.Active.Mapper.id.toString(); - } - return q; + display: function(s) { return s.label; }, + templates: { + notFound: function(s) { + return Hogan.compile($('#mapSearchTemplate').html()).render({ + value: "No results", + label: "No results", + rtype: "noresult" + }); + }, + header: mapheader, + suggestion: function(s) { + return Hogan.compile($('#mapSearchTemplate').html()).render(s); }, - filter: function (dataset) { - if (dataset.length == 0) { - dataset.push({ - value: "No results", - label: "No results", - rtype: "noresult" - }); - } - return dataset; - } }, - engine: Hogan, - header: mapheader + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/maps', + prepare: function(query, settings) { + settings.url += '?term=' + $('.sidebarSearchField').val(); + if (Metamaps.Active.Mapper && $("#limitMapsToMe").is(':checked')) { + settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); + } + return settings; + }, + }, + }), }; var mappers = { name: 'mappers', limit: 9999, - dupChecker: function (datum1, datum2) { - return false; + display: function(s) { return s.label; }, + templates: { + notFound: function(s) { + return Hogan.compile($('#mapperSearchTemplate').html()).render({ + value: "No results", + label: "No results", + rtype: "noresult", + profile: "<%= asset_path('user.png') %>", + }); + }, + header: mapperheader, + suggestion: function(s) { + return Hogan.compile($('#mapperSearchTemplate').html()).render(s); + }, }, - template: $('#mapperSearchTemplate').html(), - remote: { - url: '/search/mappers?term=%QUERY', - filter: function (dataset) { - if (dataset.length == 0) { - dataset.push({ - profile: "<%= asset_path('user.png') %>", - - value: "No results", - label: "No results", - rtype: "noresult" - }); - } - return dataset; - } - }, - engine: Hogan, - header: mapperheader + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/maps?term=%QUERY', + wildcard: '%QUERY', + }, + }), }; - $('.sidebarSearchField').typeahead([topics, maps, mappers]); + + // Take all that crazy setup data and put it together into one beautiful typeahead call! + $('.sidebarSearchField').typeahead( + { + highlight: true, + }, + [topics, maps, mappers] + ); //Set max height of the search results box to prevent it from covering bottom left footer - $('.sidebarSearchField').bind('typeahead:suggestionsRendered', function (event) { + $('.sidebarSearchField').bind('typeahead:render', function (event) { self.initSearchOptions(); self.hideLoader(); var h = $(window).height(); @@ -577,7 +591,7 @@ Metamaps.GlobalUI.Search = { }); // tell the autocomplete to launch a new tab with the topic, map, or mapper you clicked on - $('.sidebarSearchField').bind('typeahead:selected', self.handleResultClick); + $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick); // don't do it, if they clicked on a 'addToMap' button $('.sidebarSearch button.addToMap').click(function (event) { @@ -585,7 +599,7 @@ Metamaps.GlobalUI.Search = { }); // make sure that when you click on 'limit to me' or 'toggle section' it works - $('.sidebarSearchField').bind('typeahead:queryChanged', function(){ + $('.sidebarSearchField').bind('typeahead:change', function(){ if ($(this).val() === "") { self.hideLoader(); } From 7db75b8d6d556d7963b3bff4e692c83027d7011a Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 8 Nov 2015 23:14:53 +0800 Subject: [PATCH 100/305] more fixes --- .../javascripts/src/Metamaps.GlobalUI.js.erb | 6 +++--- app/controllers/main_controller.rb | 14 +++----------- app/helpers/maps_helper.rb | 2 +- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb index 1e48af33..2e17ffae 100644 --- a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb +++ b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb @@ -499,7 +499,7 @@ Metamaps.GlobalUI.Search = { remote: { url: '/search/topics', prepare: function(query, settings) { - settings.url += '?term=' + $('.sidebarSearchField').val(); + settings.url += '?term=' + query; if (Metamaps.Active.Mapper && $("#limitTopicsToMe").is(':checked')) { settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); } @@ -532,7 +532,7 @@ Metamaps.GlobalUI.Search = { remote: { url: '/search/maps', prepare: function(query, settings) { - settings.url += '?term=' + $('.sidebarSearchField').val(); + settings.url += '?term=' + query; if (Metamaps.Active.Mapper && $("#limitMapsToMe").is(':checked')) { settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); } @@ -564,7 +564,7 @@ Metamaps.GlobalUI.Search = { datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, remote: { - url: '/search/maps?term=%QUERY', + url: '/search/mappers?term=%QUERY', wildcard: '%QUERY', }, }), diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index c5535276..1cd0f577 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -26,8 +26,6 @@ class MainController < ApplicationController # get /search/topics?term=SOMETERM def searchtopics - @current = current_user - term = params[:term] user = params[:user] ? params[:user] : false @@ -122,15 +120,13 @@ class MainController < ApplicationController end #read this next line as 'delete a topic if its private and you're either 1. logged out or 2. logged in but not the topic creator - @topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } + @topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id)) } render json: autocomplete_array_json(@topics) end # get /search/maps?term=SOMETERM def searchmaps - @current = current_user - term = params[:term] user = params[:user] ? params[:user] : nil @@ -158,15 +154,13 @@ class MainController < ApplicationController end #read this next line as 'delete a map if its private and you're either 1. logged out or 2. logged in but not the map creator - @maps.to_a.delete_if {|m| m.permission == "private" && (!authenticated? || (authenticated? && @current.id != m.user_id)) } + @maps.to_a.delete_if {|m| m.permission == "private" && (!authenticated? || (authenticated? && current_user.id != m.user_id)) } render json: autocomplete_map_array_json(@maps) end # get /search/mappers?term=SOMETERM def searchmappers - @current = current_user - term = params[:term] if term && !term.empty? && term.downcase[0..3] != "map:" && term.downcase[0..5] != "topic:" && term.downcase != "mapper:" @@ -182,8 +176,6 @@ class MainController < ApplicationController # get /search/synapses?term=SOMETERM OR # get /search/synapses?topic1id=SOMEID&topic2id=SOMEID def searchsynapses - @current = current_user - term = params[:term] topic1id = params[:topic1id] topic2id = params[:topic2id] @@ -214,7 +206,7 @@ class MainController < ApplicationController #permissions @synapses.delete_if {|s| s.permission == "private" && !authenticated? } - @synapses.delete_if {|s| s.permission == "private" && authenticated? && @current.id != s.user_id } + @synapses.delete_if {|s| s.permission == "private" && authenticated? && current_user.id != s.user_id } else @synapses = [] end diff --git a/app/helpers/maps_helper.rb b/app/helpers/maps_helper.rb index 6b3dd7b0..169ee4b9 100644 --- a/app/helpers/maps_helper.rb +++ b/app/helpers/maps_helper.rb @@ -16,7 +16,7 @@ module MapsHelper map['rtype'] = "map" contributorTip = '' - firstContributorImage = asset_path('user.png') + firstContributorImage = 'https://s3.amazonaws.com/metamaps-assets/site/user.png' if m.contributors.count > 0 firstContributorImage = m.contributors[0].image.url(:thirtytwo) m.contributors.each_with_index do |c, index| From cde6eaa564b938eb4e568691838978aa8aaed8c5 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 20 Nov 2015 14:34:05 +0800 Subject: [PATCH 101/305] Update windows installer docs --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8b0a9beb..4a74151a 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,8 @@ OR create a new account at `/join`, and use access code `qwertyui` Start mapping and programming! -While we are still figuring out vagrant for Windows, there is an older set of instructions below +We haven't figured out Vagrant for Windows yet, but we have a set of manual instructions here: + - [For Windows][windows-installation] ## Contributing From 9575a62e671f3edd5094352e8c6cf0b144d7392f Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 23 Nov 2015 18:08:27 +0800 Subject: [PATCH 102/305] add .ruby-gemset file - you may need to reinstall gems if using rvm --- .ruby-gemset | 1 + .ruby-version | 2 +- Gemfile.lock | 3 --- 3 files changed, 2 insertions(+), 4 deletions(-) create mode 100644 .ruby-gemset diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 00000000..f4597680 --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +metamaps_gen002 diff --git a/.ruby-version b/.ruby-version index ac2cdeba..378bc559 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.1.3 +ruby-2.1.3 diff --git a/Gemfile.lock b/Gemfile.lock index b9ba0c6b..385001fc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -231,6 +231,3 @@ DEPENDENCIES sass-rails uglifier uservoice-ruby - -BUNDLED WITH - 1.10.6 From d3814708a93c57dfcd4afdb244f16996d7f4fac9 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Wed, 25 Nov 2015 11:59:56 +0800 Subject: [PATCH 103/305] gc tuning --- .example-env | 13 +++++++++++++ Gemfile | 1 + Gemfile.lock | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/.example-env b/.example-env index 3e8752fb..c8fb94af 100644 --- a/.example-env +++ b/.example-env @@ -16,3 +16,16 @@ SSO_KEY # for a uniq ordered list of env vars: ## grep -rIsoh -P "(?<=ENV)(\.fetch\(|\[).[A-Z_]+.(\)|\])" | grep -oP "[A-Z_]+" | sort -u > temp +RUBY_GC_TUNE=0 #set to 1 to enable GC test +RUBY_GC_TOKEN=4f4380fc9a2857d1f008005a3eb86928 +RUBY_GC_HEAP_INIT_SLOTS=186426 +RUBY_GC_HEAP_FREE_SLOTS=559278 +RUBY_GC_HEAP_GROWTH_FACTOR=1.03 +RUBY_GC_HEAP_GROWTH_MAX_SLOTS=74570 +RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR=1.4 +RUBY_GC_MALLOC_LIMIT=32883406 +RUBY_GC_MALLOC_LIMIT_MAX=69055153 +RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR=1.68 +RUBY_GC_OLDMALLOC_LIMIT=32509481 +RUBY_GC_OLDMALLOC_LIMIT_MAX=68269910 +RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR=1.4 diff --git a/Gemfile b/Gemfile index ed6a6863..d54d13d2 100644 --- a/Gemfile +++ b/Gemfile @@ -49,4 +49,5 @@ group :development, :test do gem 'better_errors' gem 'binding_of_caller' gem 'quiet_assets' + gem 'tunemygc' end diff --git a/Gemfile.lock b/Gemfile.lock index 385001fc..f1c8c7c9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -188,6 +188,7 @@ GEM thor (0.19.1) thread_safe (0.3.5) tilt (2.0.1) + tunemygc (1.0.61) tzinfo (1.2.2) thread_safe (~> 0.1) uglifier (2.7.2) @@ -229,5 +230,9 @@ DEPENDENCIES rails_12factor redis sass-rails + tunemygc uglifier uservoice-ruby + +BUNDLED WITH + 1.10.6 From 38662fcda83b2ace811844393c8fc91d694aa533 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 28 Nov 2015 20:17:14 +0800 Subject: [PATCH 104/305] split search css out into its own file --- app/assets/stylesheets/clean.css.erb | 464 ------------------------- app/assets/stylesheets/search.scss.erb | 459 ++++++++++++++++++++++++ 2 files changed, 459 insertions(+), 464 deletions(-) create mode 100644 app/assets/stylesheets/search.scss.erb diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index 94164b6b..8c3bd7dd 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -150,470 +150,6 @@ height: 32px; } -/* search */ - -.sidebarSearch { - float:left; - height: 32px; - position: relative; -} - -#searchLoading { - height: 24px; - width: 24px; - position: absolute; - top: 4px; - right: 76px; - display: none; -} - -.unauthenticated .homePage .sidebarSearchIcon { - border-radius: 2px; -} -.sidebarSearchIcon { - float: left; - width: 72px; - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; - height: 32px; - background: #4fb5c0 url(<%= asset_data_uri('search.png') %>) no-repeat center center; - background-size: 32px 32px; - cursor: pointer; -} -.sidebarSearch .twitter-typeahead, .sidebarSearch .sidebarSearchField { - float: left; -} - -.unauthenticated .homePage .sidebarSearchField, -.unauthenticated .homePage .sidebarSearch .tt-hint { - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; -} -.explorePage .sidebarSearchField, -.explorePage .sidebarSearch .tt-hint { - width: 380px; - padding: 5px 10px 5px 10px; -} - -.sidebarSearchField { - color: #424242; -} -.sidebarSearch .tt-hint { - color: transparent; -} -.sidebarSearchField, -.sidebarSearch .tt-hint { - height: 20px; - border-top: 1px solid #BDBDBD; - border-bottom: 1px solid #BDBDBD; - border-left: none; - border-right: none; - padding: 5px 0 5px 0; - width: 0px; - margin: 0; - outline: none; - font-size: 14px; - line-height: 14px; - background: #F5F5F5; - font-family: 'din-medium', helvetica, sans-serif; -} -.sidebarSearch .tt-dropdown-menu { - top: 40px !important; - background: #F5F5F5; - width: 472px; - overflow-y: auto; - overflow-x: visible; - box-shadow: 0 10px 10px rgba(0,0,0,0.19), 0 6px 3px rgba(0,0,0,0.23); -} - -.autoOptions #mapContribs { - width: 15px; - height: 15px; - border: 1px solid #424242; - margin-top: 4px; - margin-left: 4px; -} - -.mapContributorsIcon span { - margin-left: 5px; -} - -.mapContributorsIcon li span { - margin-left: 10px; -} - - -.searchHeader { - height: 42px; - width: 100%; - position: relative; -} -.searchTopicsHeader { - background: #4fc4a8; -} -.searchMapsHeader { - background: #994fc0; -} -.searchMappersHeader { - background: #c04f4f; -} -.sidebarSearch .tt-dropdown-menu h3 { - text-transform: uppercase; - color: #F5F5F5; - font-size: 18px; - line-height: 18px; - margin: 12px 0 3px 16px; - float: left; -} -.sidebarSearch .tt-dropdown-menu .limitToMe { - float: left; - width: 12px; - height: 12px; - border: 1px; - color: #000000; - position: absolute; - top: 15px; - left: 136px; -} -.sidebarSearch .tt-dropdown-menu .limitToMeLabel { - float: left; - font-family: 'din-medium', helvetica, sans-serif; - font-size: 12px; - color: #f5f5f5; - margin: 0; - position: absolute; - top: 15px; - left: 156px; -} -.sidebarSearch .tt-dropdown-menu .minimizeResults, .sidebarSearch .tt-dropdown-menu .maximizeResults { - width: 32px; - height: 32px; - background-image: url(<%= asset_data_uri('arrowpermswhite_sprite.png') %>); - background-repeat: no-repeat; - cursor: pointer; - position: absolute; - top: 5px; - left: 410px; -} -.sidebarSearch .tt-dropdown-menu .minimizeResults { - background-position: 0 0; -} -.sidebarSearch .tt-dropdown-menu .maximizeResults { - background-position: -32px 0; -} -.sidebarSearch .tt-dataset { - overflow: visible; -} -.sidebarSearch .tt-suggestion { - position: relative; - background: #FFF; - padding: 8px 0; -} -.sidebarSearch .tt-is-under-cursor, -.sidebarSearch .tt-suggestion:hover { - background: #E0E0E0; -} - -.resultmap, .resulttopic, .resultmapper, .resultnoresult { - min-height: 48px; - display: table; -} -/*.sidebarSearch .tt-dataset-maps .tt-is-under-cursor .resultmap, -.sidebarSearch .tt-dataset-maps .tt-is-under-mouse-cursor .resultmap, -.sidebarSearch .tt-dataset-topics .tt-is-under-cursor .resulttopic, -.sidebarSearch .tt-dataset-topics .tt-is-under-mouse-cursor .resulttopic { - min-height: 48px; -}*/ -.sidebarSearch .tt-suggestion .searchResIconWrapper { - display: table-cell; - vertical-align: middle; - height: 32px; - padding: 0 18px 0 28px; -} -.sidebarSearch .tt-suggestion .icon { - width: 32px; - height: 32px; - border-radius:16px; -} -.sidebarSearch .topicMetacode { - display: table-cell; - vertical-align: middle; - padding: 0 0 0 8px; - width: 70px; -} -.sidebarSearch .tt-dataset-topics .topicIcon { - width: 32px; - height: 32px; - margin: 0 auto; -} -.sidebarSearch .tt-dataset-topics .metacodeTip { - display: none; - margin: 0 auto; -} -.sidebarSearch .tt-dataset-topics .tt-is-under-cursor .metacodeTip, -.sidebarSearch .tt-dataset-topics .tt-is-under-mouse-cursor .metacodeTip { - display: block; - font-family: 'vinyl'; - text-transform: uppercase; - font-style: italic; - font-size: 13px; - margin: 0 5px 0 2px; - text-align: center; -} -.sidebarSearch .tt-dataset-mappers .tt-suggestion .icon { - margin: 0px 0px 0px 0px; -} -.sidebarSearch .tt-dataset-mappers .resultText { - width: 150px; -} - -.sidebarSearch .resultText { - width: 260px; - display: table-cell; - padding-left: 8px; - vertical-align: middle; - word-wrap: break-word; -} -.sidebarSearch .resultTitle { - font-weight: normal; - font-size: 16px; - line-height: 20px; - width: 100%; - font-family: 'din-regular', helvetica, sans-serif; -} -.sidebarSearch .resultDesc { - font-size: 12px; - line-height: 16px; - width: 100%; - font-style: italic; - font-family: helvetica, sans-serif; -} -.sidebarSearch .tip { - display: none; -} -.sidebarSearch div.autoOptions { - width: 114px; - height: 48px; - position: absolute; - display: none; - top: 8px; - right: 0; -} -.tt-dataset-maps div.autoOptions { - width: 84px; -} -.sidebarSearch .tt-dataset-mappers .autoOptions { - width: 235px; -} -.sidebarSearch .tt-is-under-cursor .autoOptions, -.sidebarSearch .tt-is-under-mouse-cursor .autoOptions { - display: block; -} -.sidebarSearch .tt-suggestion .resultnoresult .autoOptions { - display: none; -} -.sidebarSearch .autoOptions button, -.sidebarSearch .autoOptions a, -.sidebarSearch .autoOptions div { - position: absolute; - padding: 0; - margin: 0; - border: none; - outline: none; -} -.sidebarSearch button.addToMap { - display:none; - width: 24px; - height: 24px; - background: url(<%= asset_data_uri('addtopic_sprite.png') %>); - background-repeat: no-repeat; - background-size: 48px 24px; - top: 12px; - left: 80px; - cursor: pointer; -} -.canEditMap button.addToMap { - display: block; -} -.sidebarSearch button.addToMap:hover { - background-position: -24px; -} - -.sidebarSearch div.topicCount { - width: 24px; - height: 24px; - background: url(<%= asset_data_uri('topic16.png') %>); - background-repeat: no-repeat; - background-position: 0 center; - top: 0; - left: 0; - padding-left: 18px; - font-size: 12px; - line-height: 24px; -} - -.sidebarSearch div.mapCount { - width: 24px; - height: 24px; - background: url(<%= asset_data_uri('metamap16.png') %>); - background-repeat: no-repeat; - background-position: 0 center; - left: 0; - padding-left: 20px; - font-size: 12px; - line-height: 24px; -} -.sidebarSearch div.synapseCount { - width: 24px; - height: 24px; - background: url(<%= asset_data_uri('synapse16.png') %>); - background-repeat: no-repeat; - background-position: 0 center; - top: 24px; - left: 0; - padding-left: 20px; - font-size: 12px; - line-height: 24px; -} -.sidebarSearch div.topicOriginatorIcon { - width: 18px; - height: 18px; - padding: 3px; - top: 0; - left: 44px; -} -.sidebarSearch .topicOriginatorIcon img { - border-radius: 9px; -} - -.sidebarSearch .topicOriginatorIcon .tip { - right: 30px; - top: 1px; -} -.sidebarSearch .tip { - position: absolute; - background: #424242; - width: auto; - top: 2px; - right: 25px; - color: white; - white-space: nowrap; - border-radius: 2px; - font-size: 12px !important; - font-family: 'din-regular'; - line-height: 12px; - padding: 4px 4px 4px; - z-index: 100; -} -.sidebarSearch .hoverForTip:hover .tip { - display: block; -} - -.sidebarSearch .mapContributorsIcon .tip { - right: 40px; - top: -5px; - padding-top: 5px; - padding-bottom: 5px; -} - -.sidebarSearch .hoverForTip .tip li { - padding-left: 28px; - padding-top: 4px; -} - -.tipUserImage { - position: absolute; - top: 0px; - left: 7px; - border-radius: 14px; -} - -.sidebarSearch .hoverForTip .tip:before { - content: ''; - position: absolute; - width: 0; - height: 0; - border-left: 4px solid #424242; - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; -} - -.sidebarSearch .hoverForTip.addToMap .tip { - right: 30px; -} -.sidebarSearch .hoverForTip.addToMap .tip:before { - right: -4px; -} - -.sidebarSearch .mapContributorsIcon .tip:before { - top: 12px; - right: -4px; -} - -.sidebarSearch .topicOriginatorIcon .tip:before { - top: 5px; - right: -4px; -} - -.sidebarSearch .mapContributorsIcon .mapContributors { - top: auto; - right: 0; - bottom: 21px; - white-space: normal; - width: 200px; -} -.sidebarSearch div.mapContributorsIcon { - height: 24px; - top: 0; - left: 44px; - font-size: 12px; - line-height: 24px; -} -.sidebarSearch div.topicPermission, -.sidebarSearch div.mapPermission { - width: 24px; - height: 24px; - background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>); - background-repeat: no-repeat; - background-size: 72px 48px !important; - top: 24px; - left: 44px; -} -.sidebarSearch div.topicPermission.commons, -.sidebarSearch div.mapPermission.commons { - background-position: 0 0; -} -.sidebarSearch div.topicPermission.public, -.sidebarSearch div.mapPermission.public { - background-position: -48px 0; -} -.sidebarSearch div.topicPermission.private, -.sidebarSearch div.mapPermission.private { - background-position: -24px 0; -} - -.sidebarSearch .tt-dataset-mappers div.mapCount { - top: 8px; - left: 170px; -} -.sidebarSearch .tt-dataset-mappers div.mapperCreated { - left: 0px; - padding-left: 0px; - font-size: 12px; - font-family: 'din-medium', helvetica, sans-serif; - line-height: 24px; -} -.sidebarSearch .tt-dataset-mappers div.mapperGeneration { - top: 20px; - left: 0px; - padding-left: 0px; - font-size: 12px; - font-family: 'din-medium', helvetica, sans-serif; - line-height: 24px; -} - -/* end search */ - /* end upperLeftUI */ /* upperRightUI */ diff --git a/app/assets/stylesheets/search.scss.erb b/app/assets/stylesheets/search.scss.erb new file mode 100644 index 00000000..5d56d120 --- /dev/null +++ b/app/assets/stylesheets/search.scss.erb @@ -0,0 +1,459 @@ +.sidebarSearch { + float:left; + height: 32px; + position: relative; +} + +#searchLoading { + height: 24px; + width: 24px; + position: absolute; + top: 4px; + right: 76px; + display: none; +} + +.unauthenticated .homePage .sidebarSearchIcon { + border-radius: 2px; +} +.sidebarSearchIcon { + float: left; + width: 72px; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + height: 32px; + background: #4fb5c0 url(<%= asset_data_uri('search.png') %>) no-repeat center center; + background-size: 32px 32px; + cursor: pointer; +} +.sidebarSearch .twitter-typeahead, .sidebarSearch .sidebarSearchField { + float: left; +} + +.unauthenticated .homePage .sidebarSearchField, +.unauthenticated .homePage .sidebarSearch .tt-hint { + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} +.explorePage .sidebarSearchField, +.explorePage .sidebarSearch .tt-hint { + width: 380px; + padding: 5px 10px 5px 10px; +} + +.sidebarSearchField { + color: #424242; +} +.sidebarSearch .tt-hint { + color: transparent; +} +.sidebarSearchField, +.sidebarSearch .tt-hint { + height: 20px; + border-top: 1px solid #BDBDBD; + border-bottom: 1px solid #BDBDBD; + border-left: none; + border-right: none; + padding: 5px 0 5px 0; + width: 0px; + margin: 0; + outline: none; + font-size: 14px; + line-height: 14px; + background: #F5F5F5; + font-family: 'din-medium', helvetica, sans-serif; +} +.sidebarSearch .tt-dropdown-menu { + top: 40px !important; + background: #F5F5F5; + width: 472px; + overflow-y: auto; + overflow-x: visible; + box-shadow: 0 10px 10px rgba(0,0,0,0.19), 0 6px 3px rgba(0,0,0,0.23); +} + +.autoOptions #mapContribs { + width: 15px; + height: 15px; + border: 1px solid #424242; + margin-top: 4px; + margin-left: 4px; +} + +.mapContributorsIcon span { + margin-left: 5px; +} + +.mapContributorsIcon li span { + margin-left: 10px; +} + + +.searchHeader { + height: 42px; + width: 100%; + position: relative; +} +.searchTopicsHeader { + background: #4fc4a8; +} +.searchMapsHeader { + background: #994fc0; +} +.searchMappersHeader { + background: #c04f4f; +} +.sidebarSearch .tt-dropdown-menu h3 { + text-transform: uppercase; + color: #F5F5F5; + font-size: 18px; + line-height: 18px; + margin: 12px 0 3px 16px; + float: left; +} +.sidebarSearch .tt-dropdown-menu .limitToMe { + float: left; + width: 12px; + height: 12px; + border: 1px; + color: #000000; + position: absolute; + top: 15px; + left: 136px; +} +.sidebarSearch .tt-dropdown-menu .limitToMeLabel { + float: left; + font-family: 'din-medium', helvetica, sans-serif; + font-size: 12px; + color: #f5f5f5; + margin: 0; + position: absolute; + top: 15px; + left: 156px; +} +.sidebarSearch .tt-dropdown-menu .minimizeResults, .sidebarSearch .tt-dropdown-menu .maximizeResults { + width: 32px; + height: 32px; + background-image: url(<%= asset_data_uri('arrowpermswhite_sprite.png') %>); + background-repeat: no-repeat; + cursor: pointer; + position: absolute; + top: 5px; + left: 410px; +} +.sidebarSearch .tt-dropdown-menu .minimizeResults { + background-position: 0 0; +} +.sidebarSearch .tt-dropdown-menu .maximizeResults { + background-position: -32px 0; +} +.sidebarSearch .tt-dataset { + overflow: visible; +} +.sidebarSearch .tt-suggestion { + position: relative; + background: #FFF; + padding: 8px 0; +} +.sidebarSearch .tt-is-under-cursor, +.sidebarSearch .tt-suggestion:hover { + background: #E0E0E0; +} + +.resultmap, .resulttopic, .resultmapper, .resultnoresult { + min-height: 48px; + display: table; +} +/*.sidebarSearch .tt-dataset-maps .tt-is-under-cursor .resultmap, +.sidebarSearch .tt-dataset-maps .tt-is-under-mouse-cursor .resultmap, +.sidebarSearch .tt-dataset-topics .tt-is-under-cursor .resulttopic, +.sidebarSearch .tt-dataset-topics .tt-is-under-mouse-cursor .resulttopic { + min-height: 48px; +}*/ +.sidebarSearch .tt-suggestion .searchResIconWrapper { + display: table-cell; + vertical-align: middle; + height: 32px; + padding: 0 18px 0 28px; +} +.sidebarSearch .tt-suggestion .icon { + width: 32px; + height: 32px; + border-radius:16px; +} +.sidebarSearch .topicMetacode { + display: table-cell; + vertical-align: middle; + padding: 0 0 0 8px; + width: 70px; +} +.sidebarSearch .tt-dataset-topics .topicIcon { + width: 32px; + height: 32px; + margin: 0 auto; +} +.sidebarSearch .tt-dataset-topics .metacodeTip { + display: none; + margin: 0 auto; +} +.sidebarSearch .tt-dataset-topics .tt-is-under-cursor .metacodeTip, +.sidebarSearch .tt-dataset-topics .tt-is-under-mouse-cursor .metacodeTip { + display: block; + font-family: 'vinyl'; + text-transform: uppercase; + font-style: italic; + font-size: 13px; + margin: 0 5px 0 2px; + text-align: center; +} +.sidebarSearch .tt-dataset-mappers .tt-suggestion .icon { + margin: 0px 0px 0px 0px; +} +.sidebarSearch .tt-dataset-mappers .resultText { + width: 150px; +} + +.sidebarSearch .resultText { + width: 260px; + display: table-cell; + padding-left: 8px; + vertical-align: middle; + word-wrap: break-word; +} +.sidebarSearch .resultTitle { + font-weight: normal; + font-size: 16px; + line-height: 20px; + width: 100%; + font-family: 'din-regular', helvetica, sans-serif; +} +.sidebarSearch .resultDesc { + font-size: 12px; + line-height: 16px; + width: 100%; + font-style: italic; + font-family: helvetica, sans-serif; +} +.sidebarSearch .tip { + display: none; +} +.sidebarSearch div.autoOptions { + width: 114px; + height: 48px; + position: absolute; + display: none; + top: 8px; + right: 0; +} +.tt-dataset-maps div.autoOptions { + width: 84px; +} +.sidebarSearch .tt-dataset-mappers .autoOptions { + width: 235px; +} +.sidebarSearch .tt-is-under-cursor .autoOptions, +.sidebarSearch .tt-is-under-mouse-cursor .autoOptions { + display: block; +} +.sidebarSearch .tt-suggestion .resultnoresult .autoOptions { + display: none; +} +.sidebarSearch .autoOptions button, +.sidebarSearch .autoOptions a, +.sidebarSearch .autoOptions div { + position: absolute; + padding: 0; + margin: 0; + border: none; + outline: none; +} +.sidebarSearch button.addToMap { + display:none; + width: 24px; + height: 24px; + background: url(<%= asset_data_uri('addtopic_sprite.png') %>); + background-repeat: no-repeat; + background-size: 48px 24px; + top: 12px; + left: 80px; + cursor: pointer; +} +.canEditMap button.addToMap { + display: block; +} +.sidebarSearch button.addToMap:hover { + background-position: -24px; +} + +.sidebarSearch div.topicCount { + width: 24px; + height: 24px; + background: url(<%= asset_data_uri('topic16.png') %>); + background-repeat: no-repeat; + background-position: 0 center; + top: 0; + left: 0; + padding-left: 18px; + font-size: 12px; + line-height: 24px; +} + +.sidebarSearch div.mapCount { + width: 24px; + height: 24px; + background: url(<%= asset_data_uri('metamap16.png') %>); + background-repeat: no-repeat; + background-position: 0 center; + left: 0; + padding-left: 20px; + font-size: 12px; + line-height: 24px; +} +.sidebarSearch div.synapseCount { + width: 24px; + height: 24px; + background: url(<%= asset_data_uri('synapse16.png') %>); + background-repeat: no-repeat; + background-position: 0 center; + top: 24px; + left: 0; + padding-left: 20px; + font-size: 12px; + line-height: 24px; +} +.sidebarSearch div.topicOriginatorIcon { + width: 18px; + height: 18px; + padding: 3px; + top: 0; + left: 44px; +} +.sidebarSearch .topicOriginatorIcon img { + border-radius: 9px; +} + +.sidebarSearch .topicOriginatorIcon .tip { + right: 30px; + top: 1px; +} +.sidebarSearch .tip { + position: absolute; + background: #424242; + width: auto; + top: 2px; + right: 25px; + color: white; + white-space: nowrap; + border-radius: 2px; + font-size: 12px !important; + font-family: 'din-regular'; + line-height: 12px; + padding: 4px 4px 4px; + z-index: 100; +} +.sidebarSearch .hoverForTip:hover .tip { + display: block; +} + +.sidebarSearch .mapContributorsIcon .tip { + right: 40px; + top: -5px; + padding-top: 5px; + padding-bottom: 5px; +} + +.sidebarSearch .hoverForTip .tip li { + padding-left: 28px; + padding-top: 4px; +} + +.tipUserImage { + position: absolute; + top: 0px; + left: 7px; + border-radius: 14px; +} + +.sidebarSearch .hoverForTip .tip:before { + content: ''; + position: absolute; + width: 0; + height: 0; + border-left: 4px solid #424242; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; +} + +.sidebarSearch .hoverForTip.addToMap .tip { + right: 30px; +} +.sidebarSearch .hoverForTip.addToMap .tip:before { + right: -4px; +} + +.sidebarSearch .mapContributorsIcon .tip:before { + top: 12px; + right: -4px; +} + +.sidebarSearch .topicOriginatorIcon .tip:before { + top: 5px; + right: -4px; +} + +.sidebarSearch .mapContributorsIcon .mapContributors { + top: auto; + right: 0; + bottom: 21px; + white-space: normal; + width: 200px; +} +.sidebarSearch div.mapContributorsIcon { + height: 24px; + top: 0; + left: 44px; + font-size: 12px; + line-height: 24px; +} +.sidebarSearch div.topicPermission, +.sidebarSearch div.mapPermission { + width: 24px; + height: 24px; + background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>); + background-repeat: no-repeat; + background-size: 72px 48px !important; + top: 24px; + left: 44px; +} +.sidebarSearch div.topicPermission.commons, +.sidebarSearch div.mapPermission.commons { + background-position: 0 0; +} +.sidebarSearch div.topicPermission.public, +.sidebarSearch div.mapPermission.public { + background-position: -48px 0; +} +.sidebarSearch div.topicPermission.private, +.sidebarSearch div.mapPermission.private { + background-position: -24px 0; +} + +.sidebarSearch .tt-dataset-mappers div.mapCount { + top: 8px; + left: 170px; +} +.sidebarSearch .tt-dataset-mappers div.mapperCreated { + left: 0px; + padding-left: 0px; + font-size: 12px; + font-family: 'din-medium', helvetica, sans-serif; + line-height: 24px; +} +.sidebarSearch .tt-dataset-mappers div.mapperGeneration { + top: 20px; + left: 0px; + padding-left: 0px; + font-size: 12px; + font-family: 'din-medium', helvetica, sans-serif; + line-height: 24px; +} From 48ea29b13ec45f670a784c90f564fd18d43ecc9b Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 28 Nov 2015 20:39:11 +0800 Subject: [PATCH 105/305] refactor search css into scss for clearer organization, plus do some fixes to the typeahead css --- app/assets/stylesheets/search.scss.erb | 676 +++++++++++++------------ 1 file changed, 361 insertions(+), 315 deletions(-) diff --git a/app/assets/stylesheets/search.scss.erb b/app/assets/stylesheets/search.scss.erb index 5d56d120..affde153 100644 --- a/app/assets/stylesheets/search.scss.erb +++ b/app/assets/stylesheets/search.scss.erb @@ -1,40 +1,24 @@ -.sidebarSearch { - float:left; - height: 32px; - position: relative; -} - #searchLoading { - height: 24px; - width: 24px; - position: absolute; - top: 4px; - right: 76px; - display: none; + height: 24px; + width: 24px; + position: absolute; + top: 4px; + right: 76px; + display: none; } -.unauthenticated .homePage .sidebarSearchIcon { +.unauthenticated { + .homePage .sidebarSearchIcon { border-radius: 2px; -} -.sidebarSearchIcon { - float: left; - width: 72px; - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; - height: 32px; - background: #4fb5c0 url(<%= asset_data_uri('search.png') %>) no-repeat center center; - background-size: 32px 32px; - cursor: pointer; -} -.sidebarSearch .twitter-typeahead, .sidebarSearch .sidebarSearchField { - float: left; -} + } -.unauthenticated .homePage .sidebarSearchField, -.unauthenticated .homePage .sidebarSearch .tt-hint { + .homePage .sidebarSearchField, + .homePage .sidebarSearch .tt-hint { border-top-left-radius: 2px; border-bottom-left-radius: 2px; + } } + .explorePage .sidebarSearchField, .explorePage .sidebarSearch .tt-hint { width: 380px; @@ -42,13 +26,97 @@ } .sidebarSearchField { - color: #424242; + color: #424242; } -.sidebarSearch .tt-hint { + +.sidebarSearchIcon { + float: left; + width: 72px; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + height: 32px; + background: #4fb5c0 url(<%= asset_data_uri('search.png') %>) no-repeat center center; + background-size: 32px 32px; + cursor: pointer; +} + +.autoOptions #mapContribs { + width: 15px; + height: 15px; + border: 1px solid #424242; + margin-top: 4px; + margin-left: 4px; +} + +.mapContributorsIcon span { + margin-left: 5px; +} + +.mapContributorsIcon li span { + margin-left: 10px; +} + +.searchHeader { + height: 42px; + width: 100%; + position: relative; +} +.searchTopicsHeader { + background: #4fc4a8; +} +.searchMapsHeader { + background: #994fc0; +} +.searchMappersHeader { + background: #c04f4f; +} + +.resultmap, .resulttopic, .resultmapper, .resultnoresult { + min-height: 48px; + display: table; +} + +.canEditMap button.addToMap { + display: block; +} + +.tipUserImage { + position: absolute; + top: 0px; + left: 7px; + border-radius: 14px; +} + +.sidebarSearchField { + height: 20px; + border-top: 1px solid #BDBDBD; + border-bottom: 1px solid #BDBDBD; + border-left: none; + border-right: none; + padding: 5px 0 5px 0; + width: 0px; + margin: 0; + outline: none; + font-size: 14px; + line-height: 14px; + background: #F5F5F5; + font-family: 'din-medium', helvetica, sans-serif; +} + +/* main search selector */ + +.sidebarSearch { + float:left; + height: 32px; + position: relative; + + .twitter-typeahead, + .sidebarSearchField { + float: left; + } + + .tt-hint { color: transparent; -} -.sidebarSearchField, -.sidebarSearch .tt-hint { height: 20px; border-top: 1px solid #BDBDBD; border-bottom: 1px solid #BDBDBD; @@ -62,212 +130,200 @@ line-height: 14px; background: #F5F5F5; font-family: 'din-medium', helvetica, sans-serif; -} -.sidebarSearch .tt-dropdown-menu { + } + + .tt-dropdown-menu { top: 40px !important; background: #F5F5F5; width: 472px; overflow-y: auto; overflow-x: visible; box-shadow: 0 10px 10px rgba(0,0,0,0.19), 0 6px 3px rgba(0,0,0,0.23); -} -.autoOptions #mapContribs { - width: 15px; - height: 15px; - border: 1px solid #424242; - margin-top: 4px; - margin-left: 4px; -} + h3 { + text-transform: uppercase; + color: #F5F5F5; + font-size: 18px; + line-height: 18px; + margin: 12px 0 3px 16px; + float: left; + } -.mapContributorsIcon span { - margin-left: 5px; -} + .limitToMe { + float: left; + width: 12px; + height: 12px; + border: 1px; + color: #000000; + position: absolute; + top: 15px; + left: 136px; + } -.mapContributorsIcon li span { - margin-left: 10px; -} + .limitToMeLabel { + float: left; + font-family: 'din-medium', helvetica, sans-serif; + font-size: 12px; + color: #f5f5f5; + margin: 0; + position: absolute; + top: 15px; + left: 156px; + } + .minimizeResults, + .maximizeResults { + width: 32px; + height: 32px; + background-image: url(<%= asset_data_uri('arrowpermswhite_sprite.png') %>); + background-repeat: no-repeat; + cursor: pointer; + position: absolute; + top: 5px; + left: 410px; + } + .minimizeResults { + background-position: 0 0; + } + .maximizeResults { + background-position: -32px 0; + } + }/* tt-dropdown-menu */ -.searchHeader { - height: 42px; - width: 100%; - position: relative; -} -.searchTopicsHeader { - background: #4fc4a8; -} -.searchMapsHeader { - background: #994fc0; -} -.searchMappersHeader { - background: #c04f4f; -} -.sidebarSearch .tt-dropdown-menu h3 { - text-transform: uppercase; - color: #F5F5F5; - font-size: 18px; - line-height: 18px; - margin: 12px 0 3px 16px; - float: left; -} -.sidebarSearch .tt-dropdown-menu .limitToMe { - float: left; - width: 12px; - height: 12px; - border: 1px; - color: #000000; - position: absolute; - top: 15px; - left: 136px; -} -.sidebarSearch .tt-dropdown-menu .limitToMeLabel { - float: left; - font-family: 'din-medium', helvetica, sans-serif; - font-size: 12px; - color: #f5f5f5; - margin: 0; - position: absolute; - top: 15px; - left: 156px; -} -.sidebarSearch .tt-dropdown-menu .minimizeResults, .sidebarSearch .tt-dropdown-menu .maximizeResults { - width: 32px; - height: 32px; - background-image: url(<%= asset_data_uri('arrowpermswhite_sprite.png') %>); - background-repeat: no-repeat; - cursor: pointer; - position: absolute; - top: 5px; - left: 410px; -} -.sidebarSearch .tt-dropdown-menu .minimizeResults { - background-position: 0 0; -} -.sidebarSearch .tt-dropdown-menu .maximizeResults { - background-position: -32px 0; -} -.sidebarSearch .tt-dataset { - overflow: visible; -} -.sidebarSearch .tt-suggestion { + .tt-suggestion { position: relative; background: #FFF; padding: 8px 0; -} -.sidebarSearch .tt-is-under-cursor, -.sidebarSearch .tt-suggestion:hover { - background: #E0E0E0; -} -.resultmap, .resulttopic, .resultmapper, .resultnoresult { - min-height: 48px; - display: table; -} -/*.sidebarSearch .tt-dataset-maps .tt-is-under-cursor .resultmap, -.sidebarSearch .tt-dataset-maps .tt-is-under-mouse-cursor .resultmap, -.sidebarSearch .tt-dataset-topics .tt-is-under-cursor .resulttopic, -.sidebarSearch .tt-dataset-topics .tt-is-under-mouse-cursor .resulttopic { - min-height: 48px; -}*/ -.sidebarSearch .tt-suggestion .searchResIconWrapper { - display: table-cell; - vertical-align: middle; - height: 32px; - padding: 0 18px 0 28px; -} -.sidebarSearch .tt-suggestion .icon { - width: 32px; - height: 32px; - border-radius:16px; -} -.sidebarSearch .topicMetacode { - display: table-cell; - vertical-align: middle; - padding: 0 0 0 8px; - width: 70px; -} -.sidebarSearch .tt-dataset-topics .topicIcon { - width: 32px; - height: 32px; - margin: 0 auto; -} -.sidebarSearch .tt-dataset-topics .metacodeTip { - display: none; - margin: 0 auto; -} -.sidebarSearch .tt-dataset-topics .tt-is-under-cursor .metacodeTip, -.sidebarSearch .tt-dataset-topics .tt-is-under-mouse-cursor .metacodeTip { - display: block; - font-family: 'vinyl'; - text-transform: uppercase; - font-style: italic; - font-size: 13px; - margin: 0 5px 0 2px; - text-align: center; -} -.sidebarSearch .tt-dataset-mappers .tt-suggestion .icon { - margin: 0px 0px 0px 0px; -} -.sidebarSearch .tt-dataset-mappers .resultText { - width: 150px; -} + &:hover { + background: #E0E0E0; + } + .searchResIconWrapper { + display: table-cell; + vertical-align: middle; + height: 32px; + padding: 0 18px 0 28px; + } + .icon { + width: 32px; + height: 32px; + border-radius:16px; + } + }/* tt-suggestion */ -.sidebarSearch .resultText { + .tt-dataset { + overflow: visible; + } + + .tt-dataset-maps { + .autoOptions { + width: 84px; + } + }/* .tt-dataset-maps */ + + .tt-dataset-topics { + .topicIcon { + width: 32px; + height: 32px; + margin: 0 auto; + } + .metacodeTip { + display: none; + margin: 0 auto; + } + .tt-cursor .metacodeTip, + .tt-suggestion:hover .metacodeTip { + display: block; + font-family: 'vinyl'; + text-transform: uppercase; + font-style: italic; + font-size: 13px; + margin: 0 5px 0 2px; + text-align: center; + } + }/* tt-dataset-topics */ + + .tt-dataset-mappers { + .icon { + margin: 0px 0px 0px 0px; + } + .mappers .resultText { + width: 150px; + } + .autoOptions { + width: 235px; + } + .mapCount { + top: 8px; + left: 170px; + } + .mapperCreated { + left: 0px; + padding-left: 0px; + font-size: 12px; + font-family: 'din-medium', helvetica, sans-serif; + line-height: 24px; + } + .mapperGeneration { + top: 20px; + left: 0px; + padding-left: 0px; + font-size: 12px; + font-family: 'din-medium', helvetica, sans-serif; + line-height: 24px; + } + }/* tt-dataset-mappers */ + + .resultText { width: 260px; display: table-cell; padding-left: 8px; vertical-align: middle; word-wrap: break-word; -} -.sidebarSearch .resultTitle { + } + .resultTitle { font-weight: normal; font-size: 16px; line-height: 20px; width: 100%; font-family: 'din-regular', helvetica, sans-serif; -} -.sidebarSearch .resultDesc { + } + .resultDesc { font-size: 12px; line-height: 16px; width: 100%; font-style: italic; font-family: helvetica, sans-serif; -} -.sidebarSearch .tip { - display: none; -} -.sidebarSearch div.autoOptions { + } + + .autoOptions { width: 114px; height: 48px; position: absolute; display: none; top: 8px; right: 0; -} -.tt-dataset-maps div.autoOptions { - width: 84px; -} -.sidebarSearch .tt-dataset-mappers .autoOptions { - width: 235px; -} -.sidebarSearch .tt-is-under-cursor .autoOptions, -.sidebarSearch .tt-is-under-mouse-cursor .autoOptions { + + a, + div, + button { + position: absolute; + padding: 0; + margin: 0; + border: none; + outline: none; + } + }/* .autoOptions */ + + .tt-cursor .autoOptions, + .tt-suggestion:hover .autoOptions { display: block; -} -.sidebarSearch .tt-suggestion .resultnoresult .autoOptions { + } + .tt-suggestion .resultnoresult .autoOptions { display: none; -} -.sidebarSearch .autoOptions button, -.sidebarSearch .autoOptions a, -.sidebarSearch .autoOptions div { - position: absolute; - padding: 0; - margin: 0; - border: none; - outline: none; -} -.sidebarSearch button.addToMap { + } + + .addToMap { display:none; width: 24px; height: 24px; @@ -277,15 +333,13 @@ top: 12px; left: 80px; cursor: pointer; -} -.canEditMap button.addToMap { - display: block; -} -.sidebarSearch button.addToMap:hover { - background-position: -24px; -} -.sidebarSearch div.topicCount { + &:hover { + background-position: -24px; + } + }/* .addToMap */ + + .topicCount { width: 24px; height: 24px; background: url(<%= asset_data_uri('topic16.png') %>); @@ -296,9 +350,9 @@ padding-left: 18px; font-size: 12px; line-height: 24px; -} + } -.sidebarSearch div.mapCount { + .mapCount { width: 24px; height: 24px; background: url(<%= asset_data_uri('metamap16.png') %>); @@ -308,8 +362,9 @@ padding-left: 20px; font-size: 12px; line-height: 24px; -} -.sidebarSearch div.synapseCount { + } + + .synapseCount { width: 24px; height: 24px; background: url(<%= asset_data_uri('synapse16.png') %>); @@ -320,23 +375,17 @@ padding-left: 20px; font-size: 12px; line-height: 24px; -} -.sidebarSearch div.topicOriginatorIcon { - width: 18px; - height: 18px; - padding: 3px; - top: 0; - left: 44px; -} -.sidebarSearch .topicOriginatorIcon img { - border-radius: 9px; -} + } -.sidebarSearch .topicOriginatorIcon .tip { - right: 30px; - top: 1px; -} -.sidebarSearch .tip { + .topicMetacode { + display: table-cell; + vertical-align: middle; + padding: 0 0 0 8px; + width: 70px; + } + + .tip { + display: none; position: absolute; background: #424242; width: auto; @@ -350,73 +399,88 @@ line-height: 12px; padding: 4px 4px 4px; z-index: 100; -} -.sidebarSearch .hoverForTip:hover .tip { - display: block; -} + } -.sidebarSearch .mapContributorsIcon .tip { + .topicOriginatorIcon { + width: 18px; + height: 18px; + padding: 3px; + top: 0; + left: 44px; + + img { + border-radius: 9px; + } + + .tip { + right: 30px; + top: 1px; + } + + .tip:before { + top: 5px; + right: -4px; + } + }/* .topicOriginatorIcon */ + + .mapContributorsIcon .tip { right: 40px; top: -5px; padding-top: 5px; padding-bottom: 5px; -} + } -.sidebarSearch .hoverForTip .tip li { - padding-left: 28px; - padding-top: 4px; -} + .hoverForTip{ + &:hover .tip { + display: block; + } -.tipUserImage { - position: absolute; - top: 0px; - left: 7px; - border-radius: 14px; -} + .tip li { + padding-left: 28px; + padding-top: 4px; + } + + .tip:before { + content: ''; + position: absolute; + width: 0; + height: 0; + border-left: 4px solid #424242; + border-top: 5px solid transparent; + border-bottom: 5px solid transparent; + } + + .tip { + right: 30px; + } + .tip:before { + right: -4px; + } + }/* .hoverForTip */ -.sidebarSearch .hoverForTip .tip:before { - content: ''; - position: absolute; - width: 0; - height: 0; - border-left: 4px solid #424242; - border-top: 5px solid transparent; - border-bottom: 5px solid transparent; -} - -.sidebarSearch .hoverForTip.addToMap .tip { - right: 30px; -} -.sidebarSearch .hoverForTip.addToMap .tip:before { - right: -4px; -} - -.sidebarSearch .mapContributorsIcon .tip:before { - top: 12px; - right: -4px; -} - -.sidebarSearch .topicOriginatorIcon .tip:before { - top: 5px; - right: -4px; -} - -.sidebarSearch .mapContributorsIcon .mapContributors { - top: auto; - right: 0; - bottom: 21px; - white-space: normal; - width: 200px; -} -.sidebarSearch div.mapContributorsIcon { + .mapContributorsIcon { height: 24px; top: 0; left: 44px; font-size: 12px; line-height: 24px; -} -.sidebarSearch div.topicPermission, -.sidebarSearch div.mapPermission { + + .tip:before { + top: 12px; + right: -4px; + } + + .mapContributors { + top: auto; + right: 0; + bottom: 21px; + white-space: normal; + width: 200px; + } + }/* .mapContributorsIcon */ + + .topicPermission, + .mapPermission { width: 24px; height: 24px; background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>); @@ -424,36 +488,18 @@ background-size: 72px 48px !important; top: 24px; left: 44px; -} -.sidebarSearch div.topicPermission.commons, -.sidebarSearch div.mapPermission.commons { - background-position: 0 0; -} -.sidebarSearch div.topicPermission.public, -.sidebarSearch div.mapPermission.public { - background-position: -48px 0; -} -.sidebarSearch div.topicPermission.private, -.sidebarSearch div.mapPermission.private { - background-position: -24px 0; -} -.sidebarSearch .tt-dataset-mappers div.mapCount { - top: 8px; - left: 170px; -} -.sidebarSearch .tt-dataset-mappers div.mapperCreated { - left: 0px; - padding-left: 0px; - font-size: 12px; - font-family: 'din-medium', helvetica, sans-serif; - line-height: 24px; -} -.sidebarSearch .tt-dataset-mappers div.mapperGeneration { - top: 20px; - left: 0px; - padding-left: 0px; - font-size: 12px; - font-family: 'din-medium', helvetica, sans-serif; - line-height: 24px; -} + .commons, + .commons { + background-position: 0 0; + } + .public, + .public { + background-position: -48px 0; + } + .private, + .private { + background-position: -24px 0; + } + }/* .topicPermission, .mapPermission */ +}/* .sidebarSearch */ From 7336c262e395b6ce535d76f5a966ce18047d8314 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 28 Nov 2015 21:51:28 +0800 Subject: [PATCH 106/305] fix up user model json output function --- app/models/user.rb | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index cb06f646..16b31872 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -50,15 +50,16 @@ class User < ActiveRecord::Base end def as_json_for_autocomplete - user = {} - user['id'] = u.id - user['label'] = u.name - user['value'] = u.name - user['profile'] = u.image.url(:sixtyfour) - user['mapCount'] = u.maps.count - user['generation'] = u.generation - user['created_at'] = u.created_at.strftime("%m/%d/%Y") - user['rtype'] = "mapper" + json = {} + json['id'] = id + json['label'] = name + json['value'] = name + json['profile'] = image.url(:sixtyfour) + json['mapCount'] = maps.count + json['generation'] = generation + json['created_at'] = created_at.strftime("%m/%d/%Y") + json['rtype'] = "mapper" + json end #generate a random 8 letter/digit code that they can use to invite people From 47a7e161eee329aa138f128fe25c73073f8a648a Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 28 Nov 2015 22:07:57 +0800 Subject: [PATCH 107/305] fix error with mapper search rendering --- app/helpers/users_helper.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 3c08494e..69335da1 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -5,5 +5,6 @@ module UsersHelper users.each do |user| json_users.push user.as_json_for_autocomplete end + json_users end end From 31fceab45d179efbcb717399b091910e4e26380d Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 28 Nov 2015 22:50:32 +0800 Subject: [PATCH 108/305] search box css fixes --- app/assets/stylesheets/search.scss.erb | 111 ++++++++++++------------- 1 file changed, 52 insertions(+), 59 deletions(-) diff --git a/app/assets/stylesheets/search.scss.erb b/app/assets/stylesheets/search.scss.erb index affde153..607a561f 100644 --- a/app/assets/stylesheets/search.scss.erb +++ b/app/assets/stylesheets/search.scss.erb @@ -25,10 +25,6 @@ padding: 5px 10px 5px 10px; } -.sidebarSearchField { - color: #424242; -} - .sidebarSearchIcon { float: left; width: 72px; @@ -74,6 +70,7 @@ .resultmap, .resulttopic, .resultmapper, .resultnoresult { min-height: 48px; display: table; + width: 100%; } .canEditMap button.addToMap { @@ -87,22 +84,6 @@ border-radius: 14px; } -.sidebarSearchField { - height: 20px; - border-top: 1px solid #BDBDBD; - border-bottom: 1px solid #BDBDBD; - border-left: none; - border-right: none; - padding: 5px 0 5px 0; - width: 0px; - margin: 0; - outline: none; - font-size: 14px; - line-height: 14px; - background: #F5F5F5; - font-family: 'din-medium', helvetica, sans-serif; -} - /* main search selector */ .sidebarSearch { @@ -110,9 +91,26 @@ height: 32px; position: relative; - .twitter-typeahead, + .twitter-typeahead { + float: left; + } + .sidebarSearchField { float: left; + height: 20px; + border-top: 1px solid #BDBDBD; + border-bottom: 1px solid #BDBDBD; + border-left: none; + border-right: none; + padding: 5px 0 5px 0; + width: 0px; + margin: 0; + outline: none; + font-size: 14px; + line-height: 14px; + background: #F5F5F5; + font-family: 'din-medium', helvetica, sans-serif; + color: #424242; } .tt-hint { @@ -132,7 +130,7 @@ font-family: 'din-medium', helvetica, sans-serif; } - .tt-dropdown-menu { + .tt-menu { top: 40px !important; background: #F5F5F5; width: 472px; @@ -188,27 +186,52 @@ .maximizeResults { background-position: -32px 0; } - }/* tt-dropdown-menu */ + }/* tt-menu */ .tt-suggestion { position: relative; background: #FFF; padding: 8px 0; + > div { + display: table-cell; + } + &:hover { background: #E0E0E0; } - .searchResIconWrapper { - display: table-cell; - vertical-align: middle; - height: 32px; - padding: 0 18px 0 28px; - } .icon { width: 32px; height: 32px; border-radius:16px; } + .resultText { + width: 260px; + padding-left: 8px; + vertical-align: middle; + word-wrap: break-word; + } + .resultTitle { + font-weight: normal; + font-size: 16px; + line-height: 20px; + width: 100%; + font-family: 'din-regular', helvetica, sans-serif; + } + .resultDesc { + font-size: 12px; + line-height: 16px; + width: 100%; + font-style: italic; + font-family: helvetica, sans-serif; + } + + .topicMetacode, + .searchResIconWrapper { + vertical-align: middle; + padding: 0 0 0 8px; + width: 70px; + } }/* tt-suggestion */ .tt-dataset { @@ -274,28 +297,6 @@ } }/* tt-dataset-mappers */ - .resultText { - width: 260px; - display: table-cell; - padding-left: 8px; - vertical-align: middle; - word-wrap: break-word; - } - .resultTitle { - font-weight: normal; - font-size: 16px; - line-height: 20px; - width: 100%; - font-family: 'din-regular', helvetica, sans-serif; - } - .resultDesc { - font-size: 12px; - line-height: 16px; - width: 100%; - font-style: italic; - font-family: helvetica, sans-serif; - } - .autoOptions { width: 114px; height: 48px; @@ -308,7 +309,6 @@ div, button { position: absolute; - padding: 0; margin: 0; border: none; outline: none; @@ -377,13 +377,6 @@ line-height: 24px; } - .topicMetacode { - display: table-cell; - vertical-align: middle; - padding: 0 0 0 8px; - width: 70px; - } - .tip { display: none; position: absolute; From 519342a468d7a96bc6283eb113ad684e70083fd0 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 30 Nov 2015 10:01:33 +0800 Subject: [PATCH 109/305] fix filter bug --- app/assets/javascripts/src/Metamaps.js.erb | 48 +++++++++++++--------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index d15172c9..33cb15ef 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -3300,26 +3300,34 @@ Metamaps.Filter = { // the first option enables us to accept // ['Topics', 'Synapses'] as 'collection' if (typeof collection === "object") { - Metamaps[collection[0]].each(function(model) { - var prop = model.get(propertyToCheck) ? model.get(propertyToCheck).toString() : false; - if (prop && newList.indexOf(prop) === -1) { - newList.push(prop); - } - }); - Metamaps[collection[1]].each(function(model) { - var prop = model.get(propertyToCheck) ? model.get(propertyToCheck).toString() : false; - if (prop && newList.indexOf(prop) === -1) { - newList.push(prop); - } - }); - } - else if (typeof collection === "string") { - Metamaps[collection].each(function(model) { - var prop = model.get(propertyToCheck) ? model.get(propertyToCheck).toString() : false; - if (prop && newList.indexOf(prop) === -1) { - newList.push(prop); - } - }); + Metamaps[collection[0]].each(function(model) { + var prop = model.get(propertyToCheck); + if (prop !== null) { + prop = prop.toString(); + if (newList.indexOf(prop) === -1) { + newList.push(prop); + } + } + }); + Metamaps[collection[1]].each(function(model) { + var prop = model.get(propertyToCheck); + if (prop !== null) { + prop = prop.toString(); + if (newList.indexOf(prop) === -1) { + newList.push(prop); + } + } + }); + } else if (typeof collection === "string") { + Metamaps[collection].each(function(model) { + var prop = model.get(propertyToCheck); + if (prop !== null) { + prop = prop.toString(); + if (newList.indexOf(prop) === -1) { + newList.push(prop); + } + } + }); } removed = _.difference(self.filters[filtersToUse], newList); From fa67ec044473a114728272684180f71d0807abaa Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 10 Sep 2015 17:43:48 +0800 Subject: [PATCH 110/305] update Gemfile for rails 4 --- Gemfile | 3 +++ Gemfile.lock | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/Gemfile b/Gemfile index d54d13d2..c7ec0462 100644 --- a/Gemfile +++ b/Gemfile @@ -51,3 +51,6 @@ group :development, :test do gem 'quiet_assets' gem 'tunemygc' end + +# To use Jbuilder templates for JSON +gem 'jbuilder' diff --git a/Gemfile.lock b/Gemfile.lock index f1c8c7c9..d44f02fa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -42,6 +42,12 @@ GEM aws-sdk-v1 (1.66.0) json (~> 1.4) nokogiri (>= 1.4.4) + aws-sdk (2.1.19) + aws-sdk-resources (= 2.1.19) + aws-sdk-core (2.1.19) + jmespath (~> 1.0) + aws-sdk-resources (2.1.19) + aws-sdk-core (= 2.1.19) bcrypt (3.1.10) best_in_place (3.0.3) actionpack (>= 3.2) From 75554d58bf98c6df175be6367e1fa35f09e6ad44 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 19 Sep 2015 12:02:05 -0400 Subject: [PATCH 111/305] temp --- app/assets/images/audio_sprite.png | Bin 0 -> 854 bytes app/assets/images/camera_sprite.png | Bin 0 -> 780 bytes app/assets/images/chat32.png | Bin 0 -> 466 bytes app/assets/images/cursor_sprite.png | Bin 0 -> 1302 bytes app/assets/images/default_profile.png | Bin 0 -> 2947 bytes app/assets/images/junto.png | Bin 0 -> 1715 bytes app/assets/images/sound_sprite.png | Bin 0 -> 717 bytes app/assets/images/sounds/sounds.mp3 | Bin 0 -> 166765 bytes app/assets/images/sounds/sounds.ogg | Bin 0 -> 202697 bytes app/assets/images/tray_tab.png | Bin 0 -> 331 bytes app/assets/images/video_sprite.png | Bin 0 -> 1232 bytes app/assets/javascripts/application.js | 3 + app/assets/javascripts/lib/Autolinker.js | 2756 +++++ .../javascripts/lib/attachMediaStream.js | 39 + app/assets/javascripts/lib/howler.js | 1353 +++ .../javascripts/lib/simplewebrtc.bundle.js | 9802 +++++++++++++++++ .../javascripts/lib/socketIoConnection.js | 23 + app/assets/javascripts/src/Metamaps.js.erb | 84 +- app/assets/javascripts/src/views/chatView.js | 272 + app/assets/javascripts/src/views/room.js | 111 + app/assets/javascripts/src/views/videoView.js | 182 + app/assets/stylesheets/junto.css | 265 + ...tcoin-b0328dc9830028bc1d16d0901cfe9037.png | Bin 0 -> 8772 bytes ...prite-ec51351c3569a7f5ff1326ce9e81d4db.png | Bin 0 -> 3216 bytes ...prite-474fa6955c61eda7edb94a99b9152f6b.png | Bin 0 -> 361 bytes ...prite-3bb38c6207a94c9448a74511b4fbb0da.png | Bin 0 -> 566 bytes ...e-alt-dc9c162585adeaca1a436151888dcf36.png | Bin 0 -> 715 bytes ...prite-5a48c18bd4a43024b2107430f04ca3b1.png | Bin 0 -> 331 bytes ...prite-08d79863ecc88b33cf9f80db9c1e4c20.png | Bin 0 -> 543 bytes ...prite-f4c0e07ddbe314221194b9f7a8c21a49.png | Bin 0 -> 540 bytes ...prite-27adb8ccab46306ee1b8c577355105b9.png | Bin 0 -> 349 bytes ...icons-582d09c51a2c675b9716652435de546c.png | Bin 0 -> 9909 bytes ...arrow-85c83dcd70a85ab0da93d7cd966459d0.png | Bin 0 -> 145 bytes ...prite-ea6f317538820539fc90d69ad6e04c5f.png | Bin 0 -> 1888 bytes ...prite-a95e194d637af6b2fb7c4b2d768143f7.png | Bin 0 -> 1997 bytes .../edit-d6d3c4e443f674ccd3934ecc7ff5ca28.png | Bin 0 -> 324 bytes ...prite-51c03ee16025abf84bb098f2c1dbacf1.png | Bin 0 -> 1642 bytes ...prite-0330c42177b14486b57af0bad7345e06.png | Bin 0 -> 602 bytes ...prite-14cbad2531f4a52e18c94412f7027b6c.png | Bin 0 -> 2265 bytes ...prite-64737406a07e8575d5dad46c0a3aa707.png | Bin 0 -> 853 bytes ..._dark-0210718ba049662df8bb312f2dd3c285.png | Bin 0 -> 465 bytes ...light-6a3a31a73a8611a764557126a6d634aa.png | Bin 0 -> 452 bytes ..._fade-4d425bfb9a3074dd69efec71451c92d4.png | Bin 0 -> 112637 bytes ...ction-abd8f2e892ebe608b9112249167fb64e.png | Bin 0 -> 9675 bytes ...ivity-d115f352e27307a6f15b34bde2c23a5e.png | Bin 0 -> 10328 bytes ...ument-0d73e7980d2a2051880d458038a44442.png | Bin 0 -> 10250 bytes ...zarre-d6ef966323ca17b300d15bf928f9a9d7.png | Bin 0 -> 10436 bytes ...ction-f7a16921d95319680f6e2bb6d89e0313.png | Bin 0 -> 4177 bytes ...ivity-8fd2c8ee490c75c825fb022e335f7f5b.png | Bin 0 -> 5668 bytes ...alyst-8684cc2e08b319a16cf823a8b33f231e.png | Bin 0 -> 4981 bytes ...issue-70ee7d9516c0022fef321f50b03423b0.png | Bin 0 -> 4941 bytes ...redev-f5e8829e43d457c351a315057f2d4114.png | Bin 0 -> 5002 bytes ...group-559a854009fa82a84eddd55b90644102.png | Bin 0 -> 5148 bytes ...ation-3646e8dbea9fe80a88c9c4c10fa6c569.png | Bin 0 -> 5095 bytes ...sight-ea531a2ee85d45dfffc09c2269ca013f.png | Bin 0 -> 5205 bytes ...ntion-56e25f0e6c8fd23f177db99abbdebc3e.png | Bin 0 -> 4982 bytes ...ledge-6c57cad23dd95a29c2346df4a41842da.png | Bin 0 -> 5097 bytes ...ation-41b0ca6914f032cd0fcb52ae7e9d3a50.png | Bin 0 -> 4869 bytes ..._need-d0ecd6fc17881c904a6f051f13136f67.png | Bin 0 -> 4779 bytes ...issue-c5b7bbc7161edf8ab1622faa8618e0b6.png | Bin 0 -> 5230 bytes ...unity-c215d37eedf79515b3467adc1ee285f6.png | Bin 0 -> 5335 bytes ...erson-7f53a97c6b3a602427c1b02a16e08125.png | Bin 0 -> 3868 bytes ...tform-dda55c3b748f131a200e732dbbc1faea.png | Bin 0 -> 5120 bytes ...oblem-35aa5a66d244c14d96a8db6792fe1863.png | Bin 0 -> 5489 bytes ...ocess-de1c58ad413c1e1aee7a1e1b4f01795c.png | Bin 0 -> 5458 bytes ...ource-223c4adae0374e49b1a041e48713b3d1.png | Bin 0 -> 4954 bytes ..._role-6224afbcb0fc57419887248fcbc5a664.png | Bin 0 -> 4752 bytes ..._task-9a336b99afebcb13d53a35913ad4f180.png | Bin 0 -> 3187 bytes ...ctory-dbce64e422f5fca9f911ba823fb31bdb.png | Bin 0 -> 5014 bytes ...alyst-1594e823fd9c8472821f92bf396d6923.png | Bin 0 -> 10085 bytes ...losed-15267ab51fc3279b336662252acca9bf.png | Bin 0 -> 10295 bytes ..._icon-9ac6f80a6f68b0de4dc99ed7af5b9b7a.png | Bin 0 -> 7913 bytes ...ision-e0a000daab3f5f35dbf3c30c46dfdceb.png | Bin 0 -> 9807 bytes ...ample-a5e82d0be449ac0c5356bd94434b6ad3.png | Bin 0 -> 8733 bytes ...ience-3c9d8e23637dc727d2ba9af17ac4dfd9.png | Bin 0 -> 12214 bytes ...sight-a28b4a0a0da58294f563ae0a795940c9.png | Bin 0 -> 9022 bytes ...redev-0d5daeb0100f388aedb79d75bd7e3fb8.png | Bin 0 -> 11835 bytes ...n_aim-c7c18721ea09940503481f316981a5f0.png | Bin 0 -> 3636 bytes ...ument-fcb3a3ebaa3b52538e7d6c0e985d62b4.png | Bin 0 -> 3425 bytes ...n_con-a88c0a8771ec7ede189710cd453c2138.png | Bin 0 -> 3275 bytes ...ision-9edf10ba4a03f027bb605c07303dee76.png | Bin 0 -> 3846 bytes ...event-73d1c5a960a5a35f207a6a224abbd394.png | Bin 0 -> 2274 bytes ...ample-3b4c885dae7efcf5e5b6142d88adba41.png | Bin 0 -> 4158 bytes ...ience-b0db3e7821cbc26f165a08e5caff2f1a.png | Bin 0 -> 4205 bytes ...dback-09e126215e487be6a1ae3eebc338953d.png | Bin 0 -> 3903 bytes ...ctice-5b8e3e80654172b1a9fe1ffa725c79f9.png | Bin 0 -> 4507 bytes ..._idea-dca0c2071d2f498a9ac72f467dcf7b09.png | Bin 0 -> 3977 bytes ..._list-618fecc0dfb1372b1a6968033bd7b4f3.png | Bin 0 -> 3739 bytes ...media-36f4778ff6cbfd559489ca99f7a07a3b.png | Bin 0 -> 2843 bytes ...tamap-1f86febe02a2e785900ff2d7f20896e9.png | Bin 0 -> 3931 bytes ...model-8689a99c03134d3bce78d179717f327d.png | Bin 0 -> 3233 bytes ..._note-b54e40170cf8f5063fe3ada3e708f18c.png | Bin 0 -> 4575 bytes ...ctive-8d2c03c076a2d7910aec0b9d8848a338.png | Bin 0 -> 2288 bytes ...n_pro-93a72440a2704b623f767a9f81c2eaf8.png | Bin 0 -> 3570 bytes ...oject-458491d76f67e720eb0834c84dc2b2cc.png | Bin 0 -> 3015 bytes ...stion-f3258e62c738691d62250ed6fb3900d8.png | Bin 0 -> 3582 bytes ...rence-999ddabdc430b1428741536882f502ca.png | Bin 0 -> 3246 bytes ...earch-3eb7090a5ecb06e09323975a49402a5f.png | Bin 0 -> 4539 bytes ...tatus-c4f1df87aba8c5c86bfcc99610f1a679.png | Bin 0 -> 2423 bytes ...story-9a52c6f89dc4c02a0c3006763588e760.png | Bin 0 -> 4047 bytes ...bject-eaad6a93d34e2710926580563605d343.png | Bin 0 -> 2321 bytes ..._tool-16494ee93eea15810a5b5a92013fd7bb.png | Bin 0 -> 3087 bytes ...dcard-06df1730d00e7750f6d1a18a302da4f8.png | Bin 0 -> 3817 bytes ...ctice-22595f8e6fa95cb948c78dfec1e434c6.png | Bin 0 -> 11412 bytes ...group-c8ee17544a9e41433c5234586e6f9fec.png | Bin 0 -> 10581 bytes .../idea-8d0a0f846e69f33ed61bdb9e3faf6b6f.png | Bin 0 -> 11170 bytes ...ation-bd75c5a91494a9649a074f262022dde3.png | Bin 0 -> 11591 bytes ...sight-76f0ab9b04a6bd112f1899adb171520d.png | Bin 0 -> 9832 bytes ...ntion-14e542078f64d4818dc45f358b235a9c.png | Bin 0 -> 10331 bytes ...ledge-ca06041ce7aa9d8084960529f08a5d29.png | Bin 0 -> 9117 bytes .../list-9137ea315fca43a6f4249e629cd849dc.png | Bin 0 -> 10355 bytes ...ation-cea0f2a62f9d151cafdc0110dded953e.png | Bin 0 -> 9871 bytes ...iemap-090435483d05cf98c6aad5c4d09e3b06.png | Bin 0 -> 10563 bytes .../note-51ba25c12bc7b5fb5f3d2f13764f9463.png | Bin 0 -> 10046 bytes ...D_API-506643a0707df89ca6cdf9e01605e8e9.png | Bin 0 -> 3153 bytes ... Data-8fcd08ce645aeae20d8608d9c40616ce.png | Bin 0 -> 3337 bytes ... Data-d96093dff8c73ffb6d57794942b84159.png | Bin 0 -> 3718 bytes ... Data-b69ccaabeaea35122a5d92f4e874aa35.png | Bin 0 -> 3708 bytes ...alyst-75c576e6ea1f66f94ec15d1b8821eeca.png | Bin 0 -> 3529 bytes ...ining-2d55616d30b1fcb58fe79596dd36d0a1.png | Bin 0 -> 3438 bytes ...ntist-14390d5206d28dee4f6288dcb01944ec.png | Bin 0 -> 3532 bytes ..._Data-eb05171083ef900899b7390999655b77.png | Bin 0 -> 3759 bytes ...abase-1f29a341f835a0212ce41b2916a54cfb.png | Bin 0 -> 3548 bytes ...ation-61bd279cab3d08ba69dd5aaf25ea6d68.png | Bin 0 -> 3158 bytes ... Data-cf0b712efe6f5f8ca7c2eca965798520.png | Bin 0 -> 2940 bytes ...acker-53c6ee3046b04290f3ab63bb7a9bd5cf.png | Bin 0 -> 3388 bytes ... Data-d010598d67d39dcf33b31f0c4507b697.png | Bin 0 -> 3419 bytes ...apper-6daf5dfa7e2283c7284c86e27df4cf2d.png | Bin 0 -> 3321 bytes ...adata-a2ef5bf4896e7a9b9f146084067fc15b.png | Bin 0 -> 3297 bytes ... Data-30df68153ff4a0ee9c2477404ba4e280.png | Bin 0 -> 3335 bytes ...ation-b0abce3dd64f81ca3a96d8e42dc25d23.png | Bin 0 -> 3530 bytes ...artup-3c485825bcdc732418c1d03e7bd79efc.png | Bin 0 -> 3734 bytes ...rithm-c7302a013301aec08c128acc05fee01e.png | Bin 0 -> 2976 bytes ...issue-d77dfb76b9b7e5be3b9d603f8cea8667.png | Bin 0 -> 11878 bytes ...inion-7de62df7fd073dc2509636bb6abec1a9.png | Bin 0 -> 9951 bytes ...unity-d29060b764733e42ccd315213aaa5d58.png | Bin 0 -> 10478 bytes ...erson-3c753f20ecadc4267ee6cce18a36977e.png | Bin 0 -> 9643 bytes ...tform-163be434ff4103819c21828f4de0ed65.png | Bin 0 -> 10104 bytes .../pro-c633975d5df7701ae85723453a73ad49.png | Bin 0 -> 8956 bytes ...oblem-9700584ff07c14663732e128bfea5fc8.png | Bin 0 -> 11075 bytes ...stion-bb8d4d80d31109283cc8af41ac1edb45.png | Bin 0 -> 8250 bytes ...rence-2a61b6f4307ebdef735e4b43e614ae3e.png | Bin 0 -> 10082 bytes ...ement-cff838dc3d45a8553a5273fff225cd01.png | Bin 0 -> 9694 bytes ...earch-f86abb0b6acbf555a8d0f31244e99e00.png | Bin 0 -> 11130 bytes ...ource-71e7130d4e5cfa588c27bec9fc2f5bd7.png | Bin 0 -> 9661 bytes .../role-647e1d1634586e3e7a3b8ab80ec77449.png | Bin 0 -> 9207 bytes .../task-5b7a277ceefaf04aee6420774f48e742.png | Bin 0 -> 8519 bytes .../tool-a9aa41a694355d723cac130155fb6de8.png | Bin 0 -> 7384 bytes ...ctory-a8cca6e1225a46922b62c6bcda48e565.png | Bin 0 -> 8027 bytes ...dcard-595930d1e9f48c79bacef5afdfd6a469.png | Bin 0 -> 9643 bytes ...0x100-3d0f77f5d1e613e6c212e1c3acc28330.png | Bin 0 -> 180 bytes ...0x100-a9815e32a79da42d9a0047223b378699.png | Bin 0 -> 178 bytes ...1x400-4d90764e1884494ee9e1c4b1a3c66ee2.png | Bin 0 -> 120 bytes ...1x400-50dcb48f847e94ab0eec0d13778c719c.png | Bin 0 -> 105 bytes ...1x400-5225d9f0e973b03d5ef3e39c83417f3c.png | Bin 0 -> 111 bytes ...1x400-c8c49db75da9cdce80551582fbfb9e7b.png | Bin 0 -> 110 bytes ...1x400-2a7ebd433aada772c2c6efd374edcb73.png | Bin 0 -> 119 bytes ...1x100-ee51cb4c32984991223bcea7b1453a31.png | Bin 0 -> 101 bytes ...6x240-ccf2bc5085a133b936a47e17cd4fdb1c.png | Bin 0 -> 4369 bytes ...6x240-7f2fc0c521e2157c4de0be98cd43baf3.png | Bin 0 -> 4369 bytes ...6x240-9f6a81923df60e81c07cabaad31c5739.png | Bin 0 -> 4369 bytes ...6x240-427c065a98466145840a6a32f9480977.png | Bin 0 -> 4369 bytes ...6x240-b8e9df6a7b59feed26328ad5a6f77251.png | Bin 0 -> 4369 bytes ...prite-885e9390a9be873d44c96a0984d99d43.png | Bin 0 -> 1773 bytes ...prite-f88e23d28f9e38b89c50e445c4a342be.png | Bin 0 -> 601 bytes ...prite-366b5f9404c18b2282cd5bc23c8e0a9f.png | Bin 0 -> 744 bytes ...prite-e11162991ec8fe7a5a4f42d7d1c8235e.png | Bin 0 -> 950 bytes ...0x300-fff3f341b217dbdb85819c8d2aee562d.gif | Bin 0 -> 149061 bytes ...prite-05984ed738b37f874076e9651f96428d.png | Bin 0 -> 475 bytes ...8x128-530c4f73320588b5d7102f83493a434c.png | Bin 0 -> 4095 bytes ...map16-23e40fd3b933190e5a6316aadf9f3b85.png | Bin 0 -> 387 bytes ...ap32c-388ee61a9a85bcd09977849e3ca418c8.png | Bin 0 -> 1169 bytes ...ap36c-eca1b671641097cd456ab31239a59cb5.png | Bin 0 -> 1577 bytes ...g-map-37ab77d5d5320f5a64ed46c743dfce52.png | Bin 0 -> 4242 bytes ...elfie-747c9be2db7af36f5793d9dcfe61920a.jpg | Bin 0 -> 202708 bytes ...logos-f45d64b3ed356512c7e47406eedd577f.png | Bin 0 -> 18024 bytes ...prite-d624e6f0e7329b43098d355cc85cb63c.png | Bin 0 -> 2937 bytes ...prite-3d6cb2880ad26b1782f0d7cd9e64f69e.png | Bin 0 -> 2283 bytes ...prite-1b8f812b67a7de30004a31be8647ab82.png | Bin 0 -> 4193 bytes ...photo-10dcba1ad8dad7cae9d3d917e7c20be4.png | Bin 0 -> 685 bytes ...prite-5465500ab246aa35ef6838db60dd7c8b.png | Bin 0 -> 1980 bytes ...emove-b9adf357b17bfbd29054df5b15faa090.png | Bin 0 -> 328 bytes ...prite-b80d0c64c1efa5d173f50413e6dac08f.png | Bin 0 -> 438 bytes ...prite-e7633cb5ab362c5d46123bcad2faf9bb.png | Bin 0 -> 1556 bytes ...earch-98abd77b1699d69679773b8fdde0fbce.png | Bin 0 -> 462 bytes ...prite-665efab6d78cf008fdca3b2fed16352c.png | Bin 0 -> 985 bytes ...pinfo-ee5558d55cc08140a81b321246eef21f.png | Bin 0 -> 1060 bytes ...d_@2X-cd536a8a38e4e9772621660178787923.png | Bin 0 -> 190781 bytes ...pse16-6d966fd795125ebec4743ed48db123c7.png | Bin 0 -> 333 bytes ...pse32-bfff28f11853126331b999681e3b89c6.png | Bin 0 -> 467 bytes ...prite-ea7c22bebf77d44e358d079d7923ae73.png | Bin 0 -> 627 bytes ...added-14d3be639fa86b3f4333e721d524b688.png | Bin 0 -> 423 bytes ...prite-0eb7c7496df1313a7099a1ce757d4d09.png | Bin 0 -> 494 bytes ...prite-f5cb05f8e3c499aedfa156908c9e51bf.png | Bin 0 -> 501 bytes ...pic16-ab805a0124ece43870cbb8d76fa3d3f0.png | Bin 0 -> 533 bytes ...pic32-4f53c9e1c297ae26127d97b38254dbdf.png | Bin 0 -> 770 bytes ...ifier-22c3efd49bd2db196ef739ec7732b2cf.png | Bin 0 -> 450 bytes ...ifier-4195a806429f3bc61817a90c81de7505.png | Bin 0 -> 621 bytes ...prite-7026153bcdbe004edf3a62f253db6e2d.png | Bin 0 -> 7333 bytes ...0x100-3d0f77f5d1e613e6c212e1c3acc28330.png | Bin 0 -> 180 bytes ...0x100-a9815e32a79da42d9a0047223b378699.png | Bin 0 -> 178 bytes ...1x400-4d90764e1884494ee9e1c4b1a3c66ee2.png | Bin 0 -> 120 bytes ...1x400-50dcb48f847e94ab0eec0d13778c719c.png | Bin 0 -> 105 bytes ...1x400-5225d9f0e973b03d5ef3e39c83417f3c.png | Bin 0 -> 111 bytes ...1x400-c8c49db75da9cdce80551582fbfb9e7b.png | Bin 0 -> 110 bytes ...1x400-2a7ebd433aada772c2c6efd374edcb73.png | Bin 0 -> 119 bytes ...1x100-ee51cb4c32984991223bcea7b1453a31.png | Bin 0 -> 101 bytes ...6x240-ccf2bc5085a133b936a47e17cd4fdb1c.png | Bin 0 -> 4369 bytes ...6x240-7f2fc0c521e2157c4de0be98cd43baf3.png | Bin 0 -> 4369 bytes ...6x240-9f6a81923df60e81c07cabaad31c5739.png | Bin 0 -> 4369 bytes ...6x240-427c065a98466145840a6a32f9480977.png | Bin 0 -> 4369 bytes ...6x240-b8e9df6a7b59feed26328ad5a6f77251.png | Bin 0 -> 4369 bytes ...prite-dbf4a443ca9dcc2c640506a96daf27ff.png | Bin 0 -> 476 bytes .../user-5463840961a9ffbbe2c5aec5d4ee4b15.png | Bin 0 -> 1955 bytes ...prite-b54b8c52d1caa73532661887d8b797fa.png | Bin 0 -> 1936 bytes ...creen-fb75d239d74999e84536a96c3789888a.png | Bin 0 -> 253986 bytes ...htbox-4c4952789398316b94162792982e2120.png | Bin 0 -> 266 bytes ...prite-42f57ce62f6268d1c7ce274492967ee0.png | Bin 0 -> 364 bytes realtime/package.json | 3 +- realtime/realtime-server.js | 7 +- realtime/signal.js | 178 + tempFILE.js | 111 + 222 files changed, 15185 insertions(+), 4 deletions(-) create mode 100644 app/assets/images/audio_sprite.png create mode 100644 app/assets/images/camera_sprite.png create mode 100644 app/assets/images/chat32.png create mode 100644 app/assets/images/cursor_sprite.png create mode 100644 app/assets/images/default_profile.png create mode 100644 app/assets/images/junto.png create mode 100644 app/assets/images/sound_sprite.png create mode 100644 app/assets/images/sounds/sounds.mp3 create mode 100644 app/assets/images/sounds/sounds.ogg create mode 100644 app/assets/images/tray_tab.png create mode 100644 app/assets/images/video_sprite.png create mode 100644 app/assets/javascripts/lib/Autolinker.js create mode 100644 app/assets/javascripts/lib/attachMediaStream.js create mode 100644 app/assets/javascripts/lib/howler.js create mode 100644 app/assets/javascripts/lib/simplewebrtc.bundle.js create mode 100644 app/assets/javascripts/lib/socketIoConnection.js create mode 100644 app/assets/javascripts/src/views/chatView.js create mode 100644 app/assets/javascripts/src/views/room.js create mode 100644 app/assets/javascripts/src/views/videoView.js create mode 100644 app/assets/stylesheets/junto.css create mode 100644 public/assets/RibbonDonateBitcoin-b0328dc9830028bc1d16d0901cfe9037.png create mode 100755 public/assets/about_sprite-ec51351c3569a7f5ff1326ce9e81d4db.png create mode 100755 public/assets/addtopic_sprite-474fa6955c61eda7edb94a99b9152f6b.png create mode 100755 public/assets/arrow_sprite-3bb38c6207a94c9448a74511b4fbb0da.png create mode 100644 public/assets/arrow_sprite-alt-dc9c162585adeaca1a436151888dcf36.png create mode 100755 public/assets/arrowdown_sprite-5a48c18bd4a43024b2107430f04ca3b1.png create mode 100644 public/assets/arrowperms_sprite-08d79863ecc88b33cf9f80db9c1e4c20.png create mode 100644 public/assets/arrowpermswhite_sprite-f4c0e07ddbe314221194b9f7a8c21a49.png create mode 100755 public/assets/arrowright_sprite-27adb8ccab46306ee1b8c577355105b9.png create mode 100644 public/assets/browser_icons-582d09c51a2c675b9716652435de546c.png create mode 100644 public/assets/compass_arrow-85c83dcd70a85ab0da93d7cd966459d0.png create mode 100755 public/assets/context_sprite-ea6f317538820539fc90d69ad6e04c5f.png create mode 100644 public/assets/context_topicview_sprite-a95e194d637af6b2fb7c4b2d768143f7.png create mode 100755 public/assets/edit-d6d3c4e443f674ccd3934ecc7ff5ca28.png create mode 100755 public/assets/exploremaps_sprite-51c03ee16025abf84bb098f2c1dbacf1.png create mode 100755 public/assets/extents_sprite-0330c42177b14486b57af0bad7345e06.png create mode 100644 public/assets/feedback_sprite-14cbad2531f4a52e18c94412f7027b6c.png create mode 100755 public/assets/help_sprite-64737406a07e8575d5dad46c0a3aa707.png create mode 100755 public/assets/home_dark-0210718ba049662df8bb312f2dd3c285.png create mode 100755 public/assets/home_light-6a3a31a73a8611a764557126a6d634aa.png create mode 100644 public/assets/homepage_bg_fade-4d425bfb9a3074dd69efec71451c92d4.png create mode 100644 public/assets/icons/action-abd8f2e892ebe608b9112249167fb64e.png create mode 100644 public/assets/icons/activity-d115f352e27307a6f15b34bde2c23a5e.png create mode 100644 public/assets/icons/argument-0d73e7980d2a2051880d458038a44442.png create mode 100644 public/assets/icons/bizarre-d6ef966323ca17b300d15bf928f9a9d7.png create mode 100644 public/assets/icons/blueprint_96px/bp_action-f7a16921d95319680f6e2bb6d89e0313.png create mode 100644 public/assets/icons/blueprint_96px/bp_activity-8fd2c8ee490c75c825fb022e335f7f5b.png create mode 100644 public/assets/icons/blueprint_96px/bp_catalyst-8684cc2e08b319a16cf823a8b33f231e.png create mode 100644 public/assets/icons/blueprint_96px/bp_closedissue-70ee7d9516c0022fef321f50b03423b0.png create mode 100644 public/assets/icons/blueprint_96px/bp_futuredev-f5e8829e43d457c351a315057f2d4114.png create mode 100644 public/assets/icons/blueprint_96px/bp_group-559a854009fa82a84eddd55b90644102.png create mode 100644 public/assets/icons/blueprint_96px/bp_implication-3646e8dbea9fe80a88c9c4c10fa6c569.png create mode 100644 public/assets/icons/blueprint_96px/bp_insight-ea531a2ee85d45dfffc09c2269ca013f.png create mode 100644 public/assets/icons/blueprint_96px/bp_intention-56e25f0e6c8fd23f177db99abbdebc3e.png create mode 100644 public/assets/icons/blueprint_96px/bp_knowledge-6c57cad23dd95a29c2346df4a41842da.png create mode 100644 public/assets/icons/blueprint_96px/bp_location-41b0ca6914f032cd0fcb52ae7e9d3a50.png create mode 100644 public/assets/icons/blueprint_96px/bp_need-d0ecd6fc17881c904a6f051f13136f67.png create mode 100644 public/assets/icons/blueprint_96px/bp_openissue-c5b7bbc7161edf8ab1622faa8618e0b6.png create mode 100644 public/assets/icons/blueprint_96px/bp_opportunity-c215d37eedf79515b3467adc1ee285f6.png create mode 100644 public/assets/icons/blueprint_96px/bp_person-7f53a97c6b3a602427c1b02a16e08125.png create mode 100644 public/assets/icons/blueprint_96px/bp_platform-dda55c3b748f131a200e732dbbc1faea.png create mode 100644 public/assets/icons/blueprint_96px/bp_problem-35aa5a66d244c14d96a8db6792fe1863.png create mode 100644 public/assets/icons/blueprint_96px/bp_process-de1c58ad413c1e1aee7a1e1b4f01795c.png create mode 100644 public/assets/icons/blueprint_96px/bp_resource-223c4adae0374e49b1a041e48713b3d1.png create mode 100644 public/assets/icons/blueprint_96px/bp_role-6224afbcb0fc57419887248fcbc5a664.png create mode 100644 public/assets/icons/blueprint_96px/bp_task-9a336b99afebcb13d53a35913ad4f180.png create mode 100644 public/assets/icons/blueprint_96px/bp_trajectory-dbce64e422f5fca9f911ba823fb31bdb.png create mode 100644 public/assets/icons/catalyst-1594e823fd9c8472821f92bf396d6923.png create mode 100644 public/assets/icons/closed-15267ab51fc3279b336662252acca9bf.png create mode 100644 public/assets/icons/con_icon-9ac6f80a6f68b0de4dc99ed7af5b9b7a.png create mode 100644 public/assets/icons/decision-e0a000daab3f5f35dbf3c30c46dfdceb.png create mode 100644 public/assets/icons/example-a5e82d0be449ac0c5356bd94434b6ad3.png create mode 100644 public/assets/icons/experience-3c9d8e23637dc727d2ba9af17ac4dfd9.png create mode 100644 public/assets/icons/foresight-a28b4a0a0da58294f563ae0a795940c9.png create mode 100644 public/assets/icons/futuredev-0d5daeb0100f388aedb79d75bd7e3fb8.png create mode 100644 public/assets/icons/generics_96px/gen_aim-c7c18721ea09940503481f316981a5f0.png create mode 100644 public/assets/icons/generics_96px/gen_argument-fcb3a3ebaa3b52538e7d6c0e985d62b4.png create mode 100644 public/assets/icons/generics_96px/gen_con-a88c0a8771ec7ede189710cd453c2138.png create mode 100644 public/assets/icons/generics_96px/gen_decision-9edf10ba4a03f027bb605c07303dee76.png create mode 100644 public/assets/icons/generics_96px/gen_event-73d1c5a960a5a35f207a6a224abbd394.png create mode 100644 public/assets/icons/generics_96px/gen_example-3b4c885dae7efcf5e5b6142d88adba41.png create mode 100644 public/assets/icons/generics_96px/gen_experience-b0db3e7821cbc26f165a08e5caff2f1a.png create mode 100644 public/assets/icons/generics_96px/gen_feedback-09e126215e487be6a1ae3eebc338953d.png create mode 100644 public/assets/icons/generics_96px/gen_goodpractice-5b8e3e80654172b1a9fe1ffa725c79f9.png create mode 100644 public/assets/icons/generics_96px/gen_idea-dca0c2071d2f498a9ac72f467dcf7b09.png create mode 100644 public/assets/icons/generics_96px/gen_list-618fecc0dfb1372b1a6968033bd7b4f3.png create mode 100644 public/assets/icons/generics_96px/gen_media-36f4778ff6cbfd559489ca99f7a07a3b.png create mode 100644 public/assets/icons/generics_96px/gen_metamap-1f86febe02a2e785900ff2d7f20896e9.png create mode 100644 public/assets/icons/generics_96px/gen_model-8689a99c03134d3bce78d179717f327d.png create mode 100644 public/assets/icons/generics_96px/gen_note-b54e40170cf8f5063fe3ada3e708f18c.png create mode 100644 public/assets/icons/generics_96px/gen_perspective-8d2c03c076a2d7910aec0b9d8848a338.png create mode 100644 public/assets/icons/generics_96px/gen_pro-93a72440a2704b623f767a9f81c2eaf8.png create mode 100644 public/assets/icons/generics_96px/gen_project-458491d76f67e720eb0834c84dc2b2cc.png create mode 100644 public/assets/icons/generics_96px/gen_question-f3258e62c738691d62250ed6fb3900d8.png create mode 100644 public/assets/icons/generics_96px/gen_reference-999ddabdc430b1428741536882f502ca.png create mode 100644 public/assets/icons/generics_96px/gen_research-3eb7090a5ecb06e09323975a49402a5f.png create mode 100644 public/assets/icons/generics_96px/gen_status-c4f1df87aba8c5c86bfcc99610f1a679.png create mode 100644 public/assets/icons/generics_96px/gen_story-9a52c6f89dc4c02a0c3006763588e760.png create mode 100644 public/assets/icons/generics_96px/gen_subject-eaad6a93d34e2710926580563605d343.png create mode 100644 public/assets/icons/generics_96px/gen_tool-16494ee93eea15810a5b5a92013fd7bb.png create mode 100644 public/assets/icons/generics_96px/gen_wildcard-06df1730d00e7750f6d1a18a302da4f8.png create mode 100644 public/assets/icons/goodpractice-22595f8e6fa95cb948c78dfec1e434c6.png create mode 100644 public/assets/icons/group-c8ee17544a9e41433c5234586e6f9fec.png create mode 100644 public/assets/icons/idea-8d0a0f846e69f33ed61bdb9e3faf6b6f.png create mode 100644 public/assets/icons/implication-bd75c5a91494a9649a074f262022dde3.png create mode 100644 public/assets/icons/insight-76f0ab9b04a6bd112f1899adb171520d.png create mode 100644 public/assets/icons/intention-14e542078f64d4818dc45f358b235a9c.png create mode 100644 public/assets/icons/knowledge-ca06041ce7aa9d8084960529f08a5d29.png create mode 100644 public/assets/icons/list-9137ea315fca43a6f4249e629cd849dc.png create mode 100644 public/assets/icons/location-cea0f2a62f9d151cafdc0110dded953e.png create mode 100644 public/assets/icons/moviemap-090435483d05cf98c6aad5c4d09e3b06.png create mode 100644 public/assets/icons/note-51ba25c12bc7b5fb5f3d2f13764f9463.png create mode 100644 public/assets/icons/opendata_96px/OD_API-506643a0707df89ca6cdf9e01605e8e9.png create mode 100644 public/assets/icons/opendata_96px/OD_Business Data-8fcd08ce645aeae20d8608d9c40616ce.png create mode 100644 public/assets/icons/opendata_96px/OD_Cloud-hosted Data-d96093dff8c73ffb6d57794942b84159.png create mode 100644 public/assets/icons/opendata_96px/OD_Creative Commons Data-b69ccaabeaea35122a5d92f4e874aa35.png create mode 100644 public/assets/icons/opendata_96px/OD_Data Analyst-75c576e6ea1f66f94ec15d1b8821eeca.png create mode 100644 public/assets/icons/opendata_96px/OD_Data Mining-2d55616d30b1fcb58fe79596dd36d0a1.png create mode 100644 public/assets/icons/opendata_96px/OD_Data Scientist-14390d5206d28dee4f6288dcb01944ec.png create mode 100644 public/assets/icons/opendata_96px/OD_Data-eb05171083ef900899b7390999655b77.png create mode 100644 public/assets/icons/opendata_96px/OD_Database-1f29a341f835a0212ce41b2916a54cfb.png create mode 100644 public/assets/icons/opendata_96px/OD_Geolocation-61bd279cab3d08ba69dd5aaf25ea6d68.png create mode 100644 public/assets/icons/opendata_96px/OD_Government Data-cf0b712efe6f5f8ca7c2eca965798520.png create mode 100644 public/assets/icons/opendata_96px/OD_Hacker-53c6ee3046b04290f3ab63bb7a9bd5cf.png create mode 100644 public/assets/icons/opendata_96px/OD_Map Data-d010598d67d39dcf33b31f0c4507b697.png create mode 100644 public/assets/icons/opendata_96px/OD_Mapper-6daf5dfa7e2283c7284c86e27df4cf2d.png create mode 100644 public/assets/icons/opendata_96px/OD_Metadata-a2ef5bf4896e7a9b9f146084067fc15b.png create mode 100644 public/assets/icons/opendata_96px/OD_Open Data-30df68153ff4a0ee9c2477404ba4e280.png create mode 100644 public/assets/icons/opendata_96px/OD_Open Innovation-b0abce3dd64f81ca3a96d8e42dc25d23.png create mode 100644 public/assets/icons/opendata_96px/OD_Startup-3c485825bcdc732418c1d03e7bd79efc.png create mode 100644 public/assets/icons/opendata_96px/OD_algorithm-c7302a013301aec08c128acc05fee01e.png create mode 100644 public/assets/icons/openissue-d77dfb76b9b7e5be3b9d603f8cea8667.png create mode 100644 public/assets/icons/opinion-7de62df7fd073dc2509636bb6abec1a9.png create mode 100644 public/assets/icons/opportunity-d29060b764733e42ccd315213aaa5d58.png create mode 100644 public/assets/icons/person-3c753f20ecadc4267ee6cce18a36977e.png create mode 100644 public/assets/icons/platform-163be434ff4103819c21828f4de0ed65.png create mode 100644 public/assets/icons/pro-c633975d5df7701ae85723453a73ad49.png create mode 100644 public/assets/icons/problem-9700584ff07c14663732e128bfea5fc8.png create mode 100644 public/assets/icons/question-bb8d4d80d31109283cc8af41ac1edb45.png create mode 100644 public/assets/icons/reference-2a61b6f4307ebdef735e4b43e614ae3e.png create mode 100644 public/assets/icons/requirement-cff838dc3d45a8553a5273fff225cd01.png create mode 100644 public/assets/icons/research-f86abb0b6acbf555a8d0f31244e99e00.png create mode 100644 public/assets/icons/resource-71e7130d4e5cfa588c27bec9fc2f5bd7.png create mode 100644 public/assets/icons/role-647e1d1634586e3e7a3b8ab80ec77449.png create mode 100644 public/assets/icons/task-5b7a277ceefaf04aee6420774f48e742.png create mode 100644 public/assets/icons/tool-a9aa41a694355d723cac130155fb6de8.png create mode 100644 public/assets/icons/trajectory-a8cca6e1225a46922b62c6bcda48e565.png create mode 100644 public/assets/icons/wildcard-595930d1e9f48c79bacef5afdfd6a469.png create mode 100644 public/assets/images/ui-bg_flat_0_aaaaaa_40x100-3d0f77f5d1e613e6c212e1c3acc28330.png create mode 100644 public/assets/images/ui-bg_flat_75_ffffff_40x100-a9815e32a79da42d9a0047223b378699.png create mode 100644 public/assets/images/ui-bg_glass_55_fbf9ee_1x400-4d90764e1884494ee9e1c4b1a3c66ee2.png create mode 100644 public/assets/images/ui-bg_glass_65_ffffff_1x400-50dcb48f847e94ab0eec0d13778c719c.png create mode 100644 public/assets/images/ui-bg_glass_75_dadada_1x400-5225d9f0e973b03d5ef3e39c83417f3c.png create mode 100644 public/assets/images/ui-bg_glass_75_e6e6e6_1x400-c8c49db75da9cdce80551582fbfb9e7b.png create mode 100644 public/assets/images/ui-bg_glass_95_fef1ec_1x400-2a7ebd433aada772c2c6efd374edcb73.png create mode 100644 public/assets/images/ui-bg_highlight-soft_75_cccccc_1x100-ee51cb4c32984991223bcea7b1453a31.png create mode 100644 public/assets/images/ui-icons_222222_256x240-ccf2bc5085a133b936a47e17cd4fdb1c.png create mode 100644 public/assets/images/ui-icons_2e83ff_256x240-7f2fc0c521e2157c4de0be98cd43baf3.png create mode 100644 public/assets/images/ui-icons_454545_256x240-9f6a81923df60e81c07cabaad31c5739.png create mode 100644 public/assets/images/ui-icons_888888_256x240-427c065a98466145840a6a32f9480977.png create mode 100644 public/assets/images/ui-icons_cd0a0a_256x240-b8e9df6a7b59feed26328ad5a6f77251.png create mode 100755 public/assets/junto24_sprite-885e9390a9be873d44c96a0984d99d43.png create mode 100755 public/assets/link_sprite-f88e23d28f9e38b89c50e445c4a342be.png create mode 100755 public/assets/map32_sprite-366b5f9404c18b2282cd5bc23c8e0a9f.png create mode 100755 public/assets/mapinfo_sprite-e11162991ec8fe7a5a4f42d7d1c8235e.png create mode 100644 public/assets/metacodes75ms300x300-fff3f341b217dbdb85819c8d2aee562d.gif create mode 100755 public/assets/metacodesettings_sprite-05984ed738b37f874076e9651f96428d.png create mode 100644 public/assets/metamap128x128-530c4f73320588b5d7102f83493a434c.png create mode 100755 public/assets/metamap16-23e40fd3b933190e5a6316aadf9f3b85.png create mode 100755 public/assets/metamap32c-388ee61a9a85bcd09977849e3ca418c8.png create mode 100755 public/assets/metamap36c-eca1b671641097cd456ab31239a59cb5.png create mode 100644 public/assets/missing-map-37ab77d5d5320f5a64ed46c743dfce52.png create mode 100644 public/assets/monkeyselfie-747c9be2db7af36f5793d9dcfe61920a.jpg create mode 100644 public/assets/partner_logos-f45d64b3ed356512c7e47406eedd577f.png create mode 100755 public/assets/permissions32_sprite-d624e6f0e7329b43098d355cc85cb63c.png create mode 100755 public/assets/permissions36_sprite-3d6cb2880ad26b1782f0d7cd9e64f69e.png create mode 100755 public/assets/permissions64sprite-1b8f812b67a7de30004a31be8647ab82.png create mode 100755 public/assets/photo-10dcba1ad8dad7cae9d3d917e7c20be4.png create mode 100644 public/assets/profile_card_sprite-5465500ab246aa35ef6838db60dd7c8b.png create mode 100755 public/assets/remove-b9adf357b17bfbd29054df5b15faa090.png create mode 100644 public/assets/remove_mapinfo_sprite-b80d0c64c1efa5d173f50413e6dac08f.png create mode 100644 public/assets/screenshot_sprite-e7633cb5ab362c5d46123bcad2faf9bb.png create mode 100755 public/assets/search-98abd77b1699d69679773b8fdde0fbce.png create mode 100755 public/assets/share_sprite-665efab6d78cf008fdca3b2fed16352c.png create mode 100644 public/assets/share_sprite_mapinfo-ee5558d55cc08140a81b321246eef21f.png create mode 100644 public/assets/shattered_@2X-cd536a8a38e4e9772621660178787923.png create mode 100755 public/assets/synapse16-6d966fd795125ebec4743ed48db123c7.png create mode 100755 public/assets/synapse32-bfff28f11853126331b999681e3b89c6.png create mode 100755 public/assets/synapse32_sprite-ea7c22bebf77d44e358d079d7923ae73.png create mode 100644 public/assets/synapse32padded-14d3be639fa86b3f4333e721d524b688.png create mode 100755 public/assets/synapsedirectionleft_sprite-0eb7c7496df1313a7099a1ce757d4d09.png create mode 100755 public/assets/synapsedirectionright_sprite-f5cb05f8e3c499aedfa156908c9e51bf.png create mode 100755 public/assets/topic16-ab805a0124ece43870cbb8d76fa3d3f0.png create mode 100755 public/assets/topic32-4f53c9e1c297ae26127d97b38254dbdf.png create mode 100644 public/assets/topic_description_signifier-22c3efd49bd2db196ef739ec7732b2cf.png create mode 100644 public/assets/topic_link_signifier-4195a806429f3bc61817a90c81de7505.png create mode 100644 public/assets/topright_sprite-7026153bcdbe004edf3a62f253db6e2d.png create mode 100644 public/assets/ui-bg_flat_0_aaaaaa_40x100-3d0f77f5d1e613e6c212e1c3acc28330.png create mode 100644 public/assets/ui-bg_flat_75_ffffff_40x100-a9815e32a79da42d9a0047223b378699.png create mode 100644 public/assets/ui-bg_glass_55_fbf9ee_1x400-4d90764e1884494ee9e1c4b1a3c66ee2.png create mode 100644 public/assets/ui-bg_glass_65_ffffff_1x400-50dcb48f847e94ab0eec0d13778c719c.png create mode 100644 public/assets/ui-bg_glass_75_dadada_1x400-5225d9f0e973b03d5ef3e39c83417f3c.png create mode 100644 public/assets/ui-bg_glass_75_e6e6e6_1x400-c8c49db75da9cdce80551582fbfb9e7b.png create mode 100644 public/assets/ui-bg_glass_95_fef1ec_1x400-2a7ebd433aada772c2c6efd374edcb73.png create mode 100644 public/assets/ui-bg_highlight-soft_75_cccccc_1x100-ee51cb4c32984991223bcea7b1453a31.png create mode 100644 public/assets/ui-icons_222222_256x240-ccf2bc5085a133b936a47e17cd4fdb1c.png create mode 100644 public/assets/ui-icons_2e83ff_256x240-7f2fc0c521e2157c4de0be98cd43baf3.png create mode 100644 public/assets/ui-icons_454545_256x240-9f6a81923df60e81c07cabaad31c5739.png create mode 100644 public/assets/ui-icons_888888_256x240-427c065a98466145840a6a32f9480977.png create mode 100644 public/assets/ui-icons_cd0a0a_256x240-b8e9df6a7b59feed26328ad5a6f77251.png create mode 100755 public/assets/upload_sprite-dbf4a443ca9dcc2c640506a96daf27ff.png create mode 100644 public/assets/user-5463840961a9ffbbe2c5aec5d4ee4b15.png create mode 100755 public/assets/user_sprite-b54b8c52d1caa73532661887d8b797fa.png create mode 100644 public/assets/video-screen-fb75d239d74999e84536a96c3789888a.png create mode 100755 public/assets/xlightbox-4c4952789398316b94162792982e2120.png create mode 100755 public/assets/zoom_sprite-42f57ce62f6268d1c7ce274492967ee0.png create mode 100644 realtime/signal.js create mode 100644 tempFILE.js diff --git a/app/assets/images/audio_sprite.png b/app/assets/images/audio_sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..00ff9f786f24cc98c59d20bb17ecd98a7d1d7588 GIT binary patch literal 854 zcmV-c1F8IpP){wln+At7~QQx~a*ofCYbZ-DF4p5H3N_kI?z!BhmrLs4m2S9}rZ2Qnxn%)3T zkWOG6re+*_vPNoF9?1$M&X@V@`SYl%s**m-AnCdEp1=p6Now=!4lZjLBfPbPF|8(_ z4k46K%V!=x&kmt>fPyFZdmV5gJLCHj7;*PtXVO3fc>LvadQ%?&6Dn_M-(W)JyLaM9 zAQFg)j89y;Kzf8ovvnl7Fw=$WTgTiBex51m4x}VAnzA$IbdfjX+X#Ha z>m197sqv9hs>f$@;~gKl;GfF~9{>ET72j41Ens|sagVe#cj?@$+oV z)JINyWjhamg7phI{{4e1QIc()6YwPvEQAG|3_s5b5dnsTO~lGuNZ9c$$~y^=C{y5; zn|8c#MJW#w?f9-JB{@>+@ah{bJCLbt;WB38|5a4%N<;#&8gm!VmQ9ECusALdG7 zc{{ePlte(Op!freY4nDKH4pTKL~9}=B$6AG=#0-faSiahKIQ>|6z|bVjpOM=rl0#b zHHO9$J`hOp9*eINnSRbW^`Ip*-jg^_I|g*65)x{Uo~z#AlQ?h1C!zKzF`*Z{>a*yS z`v@vu;&w|ivll)p--^$k2T-wB{QCsEvhv6iDf*0WE@<#!S5_W*B1NC^4x>-__bVt} zulAHoS()4It2}Yu1|O8JSGyITQ{H(ZtBX5QzqH9|r6eSDaVNp&NH~v_SW3^(PDiBW zvBs@k)9vm+LWFOU5S@v+aLM_ElmsDj6)riSpd{XWBC}-dU}DMGVULssi6RLs28klJ gRU(0a@>hTX04%M!b?3kDdH?_b07*qoM6N<$f|KcawEzGB literal 0 HcmV?d00001 diff --git a/app/assets/images/camera_sprite.png b/app/assets/images/camera_sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..aa808e8cefbbe0c34aa2b7f06062d877c44048fb GIT binary patch literal 780 zcmV+n1M~ceP){!7~!!Q(0Q1388)hh?e1VrirWr7;1_rM0QL3^*%bOO==BDEW&2d))t5GDxE zU`KA^7&~?j1zY-ZlejPM`N!YiKP<~KIOlc|oO$yCynz2MU?5(E*;|5crt!Z`Ik_gL zFAJ9|ef)S%j67gKp0*hq-z1s6Env9?`5*kB@v=!+z;c-fW`+|=#}k405M2tpQH<}7 zzg$W2ovZQ-WWfgH}FI8K!cF@j4sju z-+}CNhmfd`AJ4V$Lw_Rk0=xh(zzgsK&i+t8nM{W4#FJ)OMqIySG#XX-$K#Ro<$+6j z4uzPcr^))phnU2<0L(f~Ss(ae+O-JG`U$>E5#pIRX9oxF2`Y@BpW^zp_P+-_GbM3? z4-r%tK`+I}v&8fh+A;7z^aO~e!tC+3#xv=+jQfwURF+}Z@u??3REm#hq-8xl!cv(B zE@ibD!4pBPIzWFX0`VcczEiA$2EO14B|ZxT4Sbz2$~}=tW{Q*Nb(^~mpE37DD?amv z6#s$|7+Z>1w9GIkFt!xYiqE4^-qjPh7pcgzD3o{g1nw#EO)GQanka;_hpQ&82`&6C zsRR{4Sajlh$icA)!lD!3Lk_NUp?VmN9M^^FVKnNaE>wwt7Ydl`x=>Y57;{iijgD?;!sDJ!00-d1E26O`G26%$5kPTpiBomZO02_o2!U+8E-)HF*xt0&k zNl+t|lTH?i?e~1Xd%hr8?Z4Z`XYx3XIfe|L!V~N79^S%FQ54k=B>-RzxOaeZrpYJ} zY3C+}0`L+;;{hJf+=kZP9sz8iPhH%3%?Ct6c}IZQ7Z1p@Vk#1wi92tMp9Xm637lHr zAFiC>2waRi*9o=7;=!|G-Z7lb#Ka!2vsXZ+W+vRx7hD_GbX1nt#H77vb0Aw#cQOfq zbU^~Cgg~+&fy7&gmbR5Z$UG#bz1^}gN-kzz%wP86Sle0>oYR|dBr)k3z{CL5;9m{` z^GZl!@y#4;AW7{k#Vdv7`)T(Ip+muHSE!7W9X)SDLXl7tk&vETJ#|z1=WY3FtNjQt09yHjt{#Nz;Q#;t07*qo IM6N<$f{&BGF8}}l literal 0 HcmV?d00001 diff --git a/app/assets/images/cursor_sprite.png b/app/assets/images/cursor_sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..ec49fec80454297817295bd1f555adcdba485dbb GIT binary patch literal 1302 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/junto.png b/app/assets/images/junto.png new file mode 100644 index 0000000000000000000000000000000000000000..5f9e3c3cc65a001097d8aa2de38afa93ce662ec1 GIT binary patch literal 1715 zcmV;k22A;hP){eTB8)X*$?6Ezuo!FCP?AT44*a=;wrDCVs2D<9ai6}yo+vD5V}YFoz}f273@Izw2&ZdkhRYQdwR#;U zg8@c^k-eHA5F@t(z`y6ey5^5ZqS~{q-HxsvFZS%&2e;_djK)$+a#kr!(B~{IdvlW| z;A$0xCKJCQ2*U=(eaRb3-x(*7bEzy+Vo88W^AguHNn9@%!XG_yVsu?;|0^%fr{zqr zr?V?SDUu1q=ug?uZxDd9E9Dwo-CZ*M9>COe>);bljP~^J1z)Xe-d}v@J*fGjZ~yt% z&S;=tdOldI)q=Tf7FXtGQ7o5E69~^ON8rf6{(iPiX!rE)_zim$Xbt^yYC1&wDI?EA zPmATfwbPA!JOv}ikL~;0t7A|VfZnUBI=;BHbkb(CdvDd&3L+Ab>G@C7_stsT+Uzs>R_`u}dHizuwt=P5I>)T?nYI^Zp7DO@# zW)pbSN4^;SlP;HB8t3R+x9Ak&*|h)Yv7?gRZf9v=M}nKG0s;`|batFgQtlT^)!`GK zZJzHRt~Tm;bLs;2?xFM$fP|b1@9Ol7Ml-eacI7*LN3($0SQMheF)(?| zAG#?AD6B(zoerr2q2`Ri8{}zp{y#`(bFk4L?U(k#pwr`bem_d35~acjr?o}0@J%y2 zwsdO5TqqQwkU`6q&XC!}M=rekj-)pjB=NA1aUpcm6c`K!KiieLE0J0<=1DIvdCF=T z(L@?we)G-lSUEqjQm<*cX0sV4&H$U)3WJ`5Lgf285ktOIgw5u_TaTO=IQP);k#r^_ zFB3;9<+AUOpMLDQDF7-VZEsZ6-2`Aj$z>P;Dc@S%%b7}9xUS@WkjrYl9{FPNh6bXn z5`Zo*e*Z~0pgZ{(t014nYDIf%EB>dQ|2Tju0jM_`=s$44clGHrq5O*aLWKzF5(S7( z#|)|X=TnLQJ5#Eak+0@tm%UlG@+R$Fu8?9tf`ZUCV>B9Z@X#T&x3u87dw(0+unY@I zQJX)HefYjms5ef{UJ9dLtK(Pq-0evdC)7$MysleTONcCdi^XVEsuXjAv&9S#18Xs% zSj-50JMWPO@4p{?-X0LQp5L$$@j8SGisGYzBAI!9PINk9+}Q`))@?}T@=!_&l*F=x z?W_8Wl=H<{l%;8YaVa$>I@;jv?Lk|+h)PaDZ*Lbw@|=vb={UY20J9FmRn%vwmrHt% z6F828xw{K8y;Dn!=UVgd&bAB1SL zp{nCG&nfhyn`%TCjz2CFs)1%b#!Xc3`dR}kR0W@W_BpUh9Orgm@TO<+|n>&Al^Fu_!*w#hnqMf>QvY5q}oFsuSqB+ zr`W{rZw8aV4W zGYU8JhRq7$YCIOQTC4+3cjs`MdkdQFb_tI@>?43dr`zpWZQHuY&1~p5ac!Gub9TUL zYgun1m*eq-zp?u5gQulmhpxqLu=|4CY`39*_pabJulKZF(3%c9jNHgCx{?h|oo61; zsHK8*@$Gj}q*A1I4d37asFZ@0hE66hf`R_Y{!ul!Y~j`D*nj^umRbClm*HLgbBz7WdqotVFNk<6gHp}G;9D9q<7LYveLwf ziLDahlTH`nj~za9$98XG+>9U1E;X z9boWp2#0YMc*qtG#p(+?ReG~V^hsS7GT%LMFP@zhSTjxW07_e6DZ!k zjiM6I5pJ<=pmTf!)TU}&3cbJnc?X(mmSrhvuJewL?smJBG_M#7%I<4VLIgrv1OPPF zdER&bi5&q)z!7i+Twmzv`$DF`tGW}d2s|oyBYmO&1mCJJ)Bn>N@i_00000NkvXXu0mjfg?TQH literal 0 HcmV?d00001 diff --git a/app/assets/images/sounds/sounds.mp3 b/app/assets/images/sounds/sounds.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..04f65df0057fa5a075b4886064022e688c709dea GIT binary patch literal 166765 zcmce+cRbtg7yq3^LJ)iJof3Q27P0r<)ZWynYOC!>>>$*xS$h^mRjIaiDQYW9Yt*b! zN~*3_jvp(-R?OFXy z1%uFA0PqhDhq#`CxMLM0f>~r%%XoR!fb|;~>RA32!lOlv+seZspoG8fD=Y51kx!wN zp39e!8w68oDERQo&m>t{5*bwjmPfG$pY&OM#~Q5$uYfqYnH?9zpgs_K`FD`VNMQ<| zL#K1LDM|UA$b;yaUp>Wtf}S}80I>HIxEnc|ybS=C-0U+;clxp>WQcoQ-cIB8bk3+5 z4hLM8?-%w32}j)mBedQnp1ScXCGVae*s&Ic79MM=0RtcQ^>w3WToW z79^Rtn%E_&#_)~GmBRXvuj1wtYuTBf>0Lw-yP7jKmRU79rX~>t&%o$w;v@ll(}#bu zz)Br4%qRz&9Qr9Ya8&!_kNbOBc}bGlgISfn`pb`66>^H>-0xl0r#v-wfBMPzPn)&Q z@#Da%9~rMTB8TJ2_{UL@926d}zcNkq(_URjZYmVVFsmO;9ZMU}-|OZAcE$+7>yq!k z57#1D&)aGE{B>;G3l$F!ncaAQK?uY>C@~h>5LE)({>=dFh_sZUpU;r#o3$0z2+yM6 zv9YCz61lnDb=xs|**5pz-!tSo57<>Lcq{&COi~mh27q@M)WawFx)BxaIwSGKasyFHnk6o);IRhl%Jq^ar=S(HF5|n27_^wcfFaVv;BOI8CgF>U+K4%t2N2OEnb+O zsH#(ZP<-V=KQE*pY&n-xV`TS;4@-Dd;B@|K->0cVmtRmB#S(1Qt#-6lB}xhJ0SUbk zYiC-io>RD)ml9emen--$*-E)DOv6|d!PVr3*tSAcCHuqs!6O7G7kl2FdW=l`m3cX< zZjM$%gkPOiwAQw0@T7q(f(^{IERRSK)&b5KZB{_%?7~VfQo;wMXaC07wx!M`*_Akl*=ILVx{^y7g@Udv90Q-t1t$;76<-rpR zVEzvrJJ>T=b00=FAio{jeSE9st?a}Y4`1!`b&j&m#b(XVT!OsM5;0P2-glH5=7)ag zn}LeRWE}{;e&4OjUh6Fh4YGplJ73AzXlovdZnd&b{mEZMfc~^ zGQMSz{a-K7JDJDr>d;|U)&wV)j<2llS5U=$T3{6qx`b@@iiq1oK~*a`N!_tVJju~4 zL3(h_YvkZ@>gSY;3Vag%%GJ+LncdV9vQ%zsK!d3-DhPWu^#H?CZ^1oc`nZu68gsa*QlM`)hRI(vXVCH7)YuzeN2RBG%#Vsk5|-U`s=2ijWV}%9hiB(rPQvi= zO4Qb7Fp$z)GBA*tVq(*#h6AbpxCe;2x=>dwbC`utJeqt)W&eC+T#6SsJa981i#q_$ zu90KH_H3aD$g%!Y#3*G-R8c2jDMCH)Xo)%43q&6R^&UH&xAGUb?7>bT2(BuVCRB7>jKWM&3^)C31QJ0 zuJVpdH0tstC}pXhSn{Nj2kl{XG2P}p-I>Qi%3nb)w|j=spM~my%Vg?yANHo;;dB9; zE31CHJMqy4(IA!RRajx0duTFu0TS0LUlV*kOf}%z{zJ6M2t1~ty~o|heVcV0xM1W_ zlwEUKvxI^wxpUHDOhOM!@}mj3uBO52F^Si zqJKy|y@LX_RxWwsAePbg7z&w<`PMj*M4@DkJa{bcvmgP|uXIIn1j}pWL}Pna1sxRl zXu=dLF8cW0R1>m7+yeNux(`!DQ(|%L=U63bd|fQO%$-o#T6Jm6Nb)CZb9HKx*2H2k?6G{;?AMC`~@> zBh@Q4;3qg8LCzG?F=_?;dzGt+6&e<(jLF4wE*ME0d!4W?8ffNvN+{iALEWJ{vn&5u z{E*Ql_oFi(B}kPI0PZ7ju3AgCLnm!s0!AsZy0AmK)HZd-eUEG8&?!B$0uH%7?5!$s z!qd!rISDc$KAO3DrhgJ{kEZ7)H9iT;bp1|d)>&1Ke9Cb{_)-0FsnIc1jz}uc2R}UE<<+U=YFT#3h7m-ey29XDsKSs7V|*;!~1AEj`&B zWJwG#&X~{jaR9Imh4uF`)7Lz=dwJFHS(X6`s^%`RHSqf56j61$s=phVxXL!V6hdxp zY;ws+TK~Q4hcYt2^radwsY^}fCAPy}u<$Q+D_evMhKY-20D)zC)4S0g^h7euSs>(7 zmc(!JT6fn}sEMK>mV#U*Au>sf-oQDnAv1~Lz*sxJDR4b9!8$>?E}u@>77j8$N>Cuy zc$JhK@{n7TmClZfdpn_XtgTTcWAF=OSS{uuD_I$|sw!sv8aWH7>l$*gy@$PD6{~cP z-~}f*s(EX(@PWR^{jA7D5DzSQj01m%de?$GIRN0Wo7w$GVuEEV`jZQBj%op=t?G9S zk5^p$a2A~#aiZx25B(r{!WV(y|6>%gcdt$bnAU;P&cfvSTXXf`yEE=qENvSeV;(#2 zovVH%VEu;CS~s_Q;wj9SRFBqLQvhIu^U*P}%v*;%;E&>$Ct!(%RqnLi?Vb;Db8(oO z-r)P>n+Q$QUupYcp8%K9Xb+-~m8d`vK^^^-$#cb#jW^_T0VBL`33OFa;~=!hqI929 zXVD&hv~BTvM_!@K5Gn;^x4wy9n#S8bJN9Bi<@=Yw-K6IY>y7>i0Ivq-n{ajM0y4eS z(Pc;ayk4TIZ@PC3OyfVG7mQEyT9}FcftzTrk@JcL_pmSK_At=P_dTDw?hp6R!d7F= zntpLfTx+>=!zb@^+Xz$vz_vzHi?F=deMdt3X%rhsN~!}ZXoDD`(JiiKp%;nyQqg+x zG^wrRC5riy4*Oxkw5=tWMP>Gg*z+$ejM}_7t^9$~c#25Cew-g2h;*01W=8wTpeRbD zo-IsS`o|(uz1tDVm)1kIbEpVWvm%!!5@2rpz z*qg`;JJryVZWxk17vUnhBWOaKX(cSrzW#|g-5lX{6Do0wW5wm%5cAC!gX|Dwezh9P zw*Ua3*ui88)VL0C`fi8z6AlZ$(;m8ive9gx>D|EzX*xb$EPSRrq(AXl2#DEm zYcJ=&cgyDt?zGqi5bP$jmEEUYq?-8dT24%{v>%(yOJlcnjeSPPN2kx^gA*~yxlN9? z3BV<$JQmU*p}yHd5D*ro{UvfD^`yqS90Sc<#;3=DucN9%m$?GeMe^Bo$kbL!P%2~D zuB7Rve@~eJ;0|Lfd1c8@420{qMmH0`uFFI8s*p%0IF{zObQfyzf$uJdviFk?G4KtV zDYcbKcVUkoS$?;k-(9*?1AvoPtOzoGCIj`)EwREtv7|m;EFTKWS;>!1ykkLjE|BmA> z%(ic>mnyZ3t69(V?7Ia*PaIlNLo(FmUGzGse#)4ix}TNlEb@hj z+>$1S5h~Mp)|PDWoqGSq;l9se z_GT?>;S@c7O}CfzT$fQ+l_rgh4>bCjK53WW=7Nu5Z_TKg!SJflx(vAR*MLR-avzE= zjQ)qA$`MX|p*c7v{-^qPRl@-dPsgn(?S+H>H;v9`=6M|k=kGHHHL4XHx~`+a;qSSA zkejuzdv(`O82A%02}wOq;M8XG^r_^}*t1QSRF{M}xY4y4Xy4yS^w527&|1LaEN*3W z7ubtETH2LAiPw!AvTv6Qv>|v4w`tvA&`N_of&XnfrUKd_FZYmB@gOXS^rO~gR0<-k z65POhjoe!(2FdK8fMf~N(KXm&)@VqCS^0U#TIBK8HIDWqNte#uTFXos&H;fc=A~dM z6+C{x1-By&hkM5>{S25_O3KpJ%fLML*XJ}d@YU@jM=tss|tVmk(u9(!k^ z)ziFI-_1u4QtKRwL`%aFDf-f!`Plf#-s z@M5hj0vLf|+H}KTOYGpA(1j*DEAjqXgA(%zv`jj4H@%tSsTo$CJ_oK{nRn(vP|x_k zSly#`9?JaS4l}{6m%v}2-6<+I)+|1`o}d%%lB^x-Jrp=N6ed8FkqC1avkKlSeDt?+ zqfMpU)4yY){@^q}xs#SxblrRQaa(%pqkpN?@=>@SCWNqX+YwO(?Oyt8y3}r;*Sxx} zk!#0d+|b|5z+8m}Fr{H;zN|z$@Bi@|EzUkILcflQ>`=1OZp6C0Ga_R|_r!EmawWby|0#(R%(8Eo65K4s zxh8$(af|hiyn2=IPfGNm$Kr;6#VHUhqwIoXkzjyP z>p>7b%|+$`PD4H2f;nc5&cw7AkJWSCj(AFg3niZ95XJ|`(j9R6)JpB5yvV{4H2|m` zzqe2IT($&F=36Zqhaam|?G(|zYn{k8b96o%l%N?{wo<-5qIQs$8%9r zi2QQCj{{DP;>b_k6S#M9M>Yt!`mJzqS#sj#hOJuZCRY#&e$-fF89UX`GadxSy}`JN z^^Uqh{PKTdy!?fkj`K}F+avw$^bMYukGX{t9G;EF2dk&vg$U9x_tVd>G}VFx@@lJM9HmYGp!iF^|)*2-neqXJeXn$cvHE8_YmmggC{5&c+6YZaL0lgW>@`F>fY7ydRn=! z7I)Q30a1CBH2-VC92z%fbUSmh?F_9Gz9jfVKFi>qSPbhw33lB^4>$TPl$g3Ru%(~l zV+rq?x_RXnMOsbJ*Rd6^LniyziemM-I_bnae|5b7eoxwgZFjeE(Q*GOYg0}a2hHj> zxJwyy9YJKEG3M*R?rPI!5BYay$mF~AiMkz_C&dbB|iJ%c8-!_V*i0W`hY!px&d zQNH0rI|qtMk(WWA3b<)}?w1bA*Ci=keX;V0iv?BX2g4q*YWoNi_a1`-3%=1yRex-r z_xU975LoBYeSw(UUb>sd$a7uv{@Tv?d`$DuSrT~woRCL5MDNqC4f9_kSA+*2V;noc z982il-Asu-&V+2D+wRZ@)IW113aL}xCo=HPe7W%sF&qUxp|lmr3c<_K%21He9rj>n z8lb(W$(ns%fvfv6wl0r8f)qGyPp?`fQxucDqi&nsZ4!Hv;9p%}GxND%>-1Gzql#7E z5kiPI^70h^BJz8o+gjwoIIS;>aS0e_9!ZFDy`-TxxSsidzpV9K`VPGB<`JKN5_Aj2IGd&oJFvlRaSizdqt@ zP&bHvgDF{_!Smhi+#KE{<9O8Kms?3qd^{AotE)Ng8rK|<8asQS6_Fok*kMJE~qTYyK;%3 zc`nLX_*il5mp)zy3o}r$7`_kwX6Ua@m(dlAUoP*RTV&MP5E)DDj%DDtc49y8JXR*aAzPWp=5%=1B;Ubdoh*iA_{U)LFqmcl*1bmaIfJmtMK~kUHNZjrp zMtWW?Fqy8&evQ?Ct~mcq%80y-Eq%m3_c*YZU@tLG-1t-)Xi}tXx7B~c01{3Bm2qee zjat&jNcn&8>=JDI!eiT#YmOj!DI=YYtjQ5p^Y<$cXNWQ4S|1eqg2i)(mCO+aGM5eT ze0geave1;j6LPkjd2(Z~74)}!-_Yv*+Wi{`LU_tDdpY5O0#CBy1qPe^^g2S&SJmy{ z+%%BPmy_qf#HqCWVOG{{&;*}CvS?d}Wehw9H*85z5}}%XAn=-+7gS&k-#yG!`7Vfq_=rBVL^sEF=n#;s$pnEirOI)};2Zvs% ztGk&#>MQ{M9oF@7Ppp+~=$ThU?t*W^z|?&G^Zz1T_ZKS>;U+CiAR^8?r$g+~HFDGVBY<);fsrF!HgKi{*2RZ<^! zgr(^bcO%**wMFrR8&PE!P_N2IjP5==Bw{S=i1Lc8H-i*pI0n09nzpIsMKo$ZK_<|e z)+DuLoJ|Hvo7>!6lm3UBl77L#4ra#1y2%?}>!G|&&bl3ri+j2V-!|_LFE0%ov4u`Z zk69>4a2@NSrbtw_Y?kz0TlCG`%uCJby};_RTP0gSc^nlkt4=bVm5$;kziFkl@AVj2 z%PT`?jPz?@K`x zPC=puFwL;jFg#AJl}nT_%!{JG=8}NiDi-{g;){Zi1XN|4&0U38b?D!7Z_QlaBNUI= z61D&dw3tr!qiE*KNnCSp?yr`Au+| zbumfbQl8nR6O>#LfXeQwoFCxMctKd~sG2N;9HHl~qHe8rj+})G$gg07=Pw`mM5NlP z`nyH>-1-p^F(dC7{dF8)!AMnA93dK~cE)4C>9TE}ZI;$=os)>~_qOq9TP74bnxc6l zdRz3$uuOEt8*B-(oE-?aY%8ha*0hsCm-Cf4af-d!~a|DL%VPO5f7h8Xf7 zr$g9>i1*xDVEg|mD!k5Zf=`GJiZ0jpbjNn?QLVtW2 zi}varJF?kk`+E1RpH|}b_U>FGx1QK@g0b%)qWgy?S&WhBRl_xq`fMUh&N!Y0B92MF zNWJyX8l~U5ZhWUrO||ibA*{UI=`t3n58CAEH|B}C)mf4p6iwP&)ZARH-$YWRa1Wn- zcTgJjJ9=(JMM0@})x&j^ih|;R*`{{qKta@|a30Syi@<$m6JKvKMK`Gzp{1n=N(3&C-O|bB29{H%jgvfP1)uRhz=Oyjzw?%uvWTsN__FpgLr$H8WKz- zmDWYzz>t4i%Nk3$U)+rb`)PyEWsO)=b+Xl>{pgJ5uGQ_601XNKN3atUsinFk@nkt_}eqbA;d@9OWWYG zkCs9I?in7RjLfE-(SA+HmtRgPB-Kr^Hmf_$py-(vH>heI3yGo)koxv5vzg(i=x_~o z1fJ(d#7?34lLwYUBP@&s+&M4m`0h1iyoRGG zg8+b~mPg*DYj2<{!8~cHAFk2W$Kt~E5dEz63S>KUs=aCe@i--uo1RH)ol#=$PjSLw zhIv;&R}6`iL6dlR3$DLJc7>0CYQAHUM9N*<5lu>;)LCagt9b;^J{Mo0i4{ARii>0K z`j-dR<@|DWjof!o&#{pGBoXS7`i*1@6<)ESrWY<-xz$IkWuk^BxU0Bcv*3+i&@d|0 z^)2S?LXViQ>65_wVpoLeA*f%`<>i#^EcQ>5#Hx9Pn22YB@l*d&NiFr{KHJ z-hk)pZVN@CzP78Bao0^^E4d;_i^QZKMGdW4!tyrr1wNNij!kcGx@E$rBmWUsQqiGV!I|GoVC6!%HQs}M&}EQP(40b9g@JII4%MX^ z;qGC-(V$dm-urrn;b8LY+sS65o`?|og9?7wBHw~@;~d5;lw{X>G_k{D)wn^7W^b$o zfS(I_A9|%fd|je5W{7-+My|WCGlO$iJ`9Ulhs1u0iGgo3KWC5EUF3MJwBaKL9564G z{&AKRyGCvs+N-T)+rdZQsJm@o!S9(rByr|NsB(WVIU7b#NY%J}o3Au6AZ%svd2GTk zj5R~O(#sah4vB5&5@sJxaOE(QwhfME5U8-J-82{fec)Ei^c9m-oRI1rs+d3aNh$4| z17450R-V8ny|%*x>Qcq?I@nEqFr(_oa2wI=^oW!GlA`9b2sJW(mk_SVWtz{csMr6+d8Wk&-^#QPLNpIv-XcP|?))GXpYcq&*->{ECExq}-pb*)oYg)psg>dv^#7hI5 z-fVE8_7W07wK#I4-f$}hxkOpFMAAP;|Iyk04nuHf6wBsFo2y;?*){T#Q>OI!y7q7^xLZhaa39{}i6|*Q9))@cXbx9vS+Jf7P_}*ATZ`JH)vIM ztNP_XyGmDGp*-uAev`T|TiMkQydP;KLrA`zkqa?u1^tj5dVT>%m2u0Nr~MDPC%u~L zgv8PQr{{ekt=#K0BnxvP$<^s#E~zJ6P9Kln6ISu>0t()1pv0873bh}4)uJsrgP}O+ zX^M?6M$xeytEot9gqGXSlVsr)&x~M{ol!Wjbf62ZKF;?47Y)YASXQT#)z@1HqX@j5 z&)+Nkh@vS|xBT+I9XlcTvT4|}>65r)qM4iY`ua#RvHo(&M+XO8rVpM;D)Jv+i+>&> z=5U@EAfo_fKXH>J9Ka?-QE?)!u1_lo?*UcklFD83%);0$yQJW?6--YXvO1+r#4ngb zCE_Rq5k%^aw}zw~zQjj-(BS9gG6Rt^QRyw#=^Mln?u85VbMCr9E0(bdF1)$Zfe^i= z@I1kwA^@lSOVBZfhnFJ8oJ$Yv2QNrCu;M6xmhRgb1O&c&s9q{^jocQtmq*yPBawMj z9rjRKg_jGrcWm#SYrMe56M#R#y}7x5;?MiMrGCvl(O&@Qx|+2{H5wr3^lUXb)X>7_ z!KT4zst|X*Iujd{6v@Q+Y^sQ1RStxwusKbtX{BqCk-S%s6>y={LgH;?YKx`>U_E|y)N)M|HE6ZfxFTo*q(iO9yl!6r?qs5*xztTdw6cq-Z?CpP6co$#uNI|NG?wdQWw=|P*bDx`mXJ6PLVL*7v}Gfu{ZGnu zmq-N(N~$Dv++%)`dq7{0IEMcx%HH)CRX?q^a)(_v&9JWj^~1eCgq^!QezkGWrcc9y z4=UM&dn>tqbIO1g%|twyIw^U)wI!~#*>Gc%BZu7w-YGh*(ILzv^XYC<5rAE;3T2i!wtC$fZUi&Q+X~i zOEOvpS2Cr^weP0DP?}mXghq`75U$@q#ZQArl)iUj7Po@bEPrvAK{Mq2khU9pt=S+#iE({OK)qmw))XVjnN_BZb8nR{xWpav~c4%dprV+-H=OwFbMArvkjXF#Xi z%bB!8>KMQuC_kGRVAzvSxjh1GW8O{9?DgiWp&Rd!ntcS(rKRvwHva`RAkuIr3Hn)# zf^k0xwXA#9g5ACOQ|WMNz4^_CC1wUq#eHx8TFRu1(%g9eNZ{(vx1mt(8$we;nO-ox z*PtVaCcyMziam_;N9nHk@HR<{5O=*r1Jh~1zF3fhgJ)6~RRmoOO?#y4T#<+7z1QkGu`wDR?EKTq)SG(S5Z z)X%m}49qp`d@~(v@p!V$*WjPT9b)w(miz;QCD4sXVB<;iYN;#(WJ9WdG>)qLQbhbg zFQ||oMHTPT#ArNqykU@%gR*VT5h$9;`RLS3B3?>acW}K|Hz|7la?It$$h_5rlM^!M zFr-J}sFSPrfVE_guva6u&n33p9NAu`51S22v2SO25K4=+>(NPqDx;OkKf-MaC>TPD zsU#(p+#XAB&+7hYC7ayOb|bu#Iq`cCCbm#dMsHf3^d=3(SBFRdW7uBf8NlUM2YSs9 zW)|*?I)-6h>bTcBX8yKgQ{P-Lf3sGt&urDNzrBvkDjW?O{Wbf^D?2}dkB|MwoO#)c zi_5#01{llDJIn%~lU#YL)sA2Wk>IyPLL?k@UhO1wL@Ps1rd&v_dh9?CIarX<|T z6Ca)3@@~?a{|c{1WZ^gcGJ7+g_zp|X)Gq5p#LnK@rS_!!a@G9H?eeNOe6^dSTd$2j z*8MFmon>t@00J9~MW&UtRBY3uwWnCjhgq_D_DCWxiHN}x0et*X-gSqDofWkvIpc4ZxH%(6#)rK__@RQloB=P22-h z>5m{KpCLwHtFW@3(FUs@-#J}{i*x@r^Cdn#V4{Alyw+lLUusT;bfgQ^#T9@p?^YNG zyE;?1`m<8&(fbMbJy=w1*p|4zz#N>2Zh5sZBSc-ON0ct}1USfUy6v!j{zvmgGh+6h zG|`?Llfl*UZwn5?9fqvU&Scgw`3%RWnDC$7lDvl@a9n+BkHljnr&)~E+KAZ(@*q(B zWG*YUuo!S}!3R9g5>OO)m=eV!_s{>`0(Y~A$WHlZQ*h`WWPu6Bm)!lW(S`G)+XG|< z*Yk{sZk6CFR1{WmQmj6)c7D~`E)9E^`%A}ca+vfra+`?WcJ(q=5Jc>Q@LY0A!$d(nwW4&%jzR}YrQyLLimCm7EsbppYk zEpF?AO^KsGc5bD&Gd%hPW<6Y9I$=1as>i*lzB$XZ-la^e=BoKJP*8*OZC_2g;oVi$ z>MW8sfP@b6gIplcP5wH~jCTlr-?y@$5Z9xfW34%+oyi}IUALSMq|{SCOw7L~%yhIv zS@wvdbfP`K4IP5})F#)rhCYCdt%W9NGq8|)bRLo3$a@nKk`KjD_?GaKu(!2d@hFpqnUMb8P@tQETjPE+B0WrjlyTFf++c*uS8RkfvsHySK8~7> zRL{-x{Y!I{Gj%NDo^BSyE|BIot#{LT1fOi(adM5^2BOykW5cpXCnwFz+NToFK9Wdc z>s=9g_#H1i+Mmnlx|>AZll0Oi9;og5yA_{*vCLvqrq}YLKKKkQ76(HvV_l*q22UiZndksQgPKtzl{Iz({R^`boU47}r_aD>MlX5Qs^mpub(9Ee-=0p3wieg9+ zeI0DE3oMm-m)yMM6>meOOswh){WB~MJ2{t2`q zbpjwn%CGP!8}Nn&@vZaPU!_sq(L}PYDYmp0w24i&r!uz3U(dCYsE0KJ9y|YY6;4ka zlWCv$6K(aOJA*OcZi=F#mN~?xEdJist--gyy_4>DyELkhoL(}c3_w)icEfbnZVy`K z3r{)`3pbpwId3RV3pjkyUkPmM1xoEBOQR{Nngqz2y~XLve?^yq9CFYs>VH6YC4wi@ zoB8qgz4G)unj+zPCYrwm^_=Q`S@QyK*mkQW4lT zfVt`KIKy*%ck1n|;+dA$hnrM#=-zou;U)@4(Pesivzt$k#92z)ZfyqS5QVMMC86_6 ze3TOWw^$2zd@lw4_TzV{GM060$k^r;YVbSyZpW|kCti_Pda#lA;4K;G71m-R<86tY z@rA12Qkv0S+Kpg~v1PUKH`@gqsuaYGV$7+dSq~;+cRxK0pWzGw4#=SV66uyzJ|G6k zm;tS!!(P)6Moa_nqn6Y(Mq$7$={1C}RCq_}=f~hSZuvbBnz2+;n^&Aznk>v!67feHz?W$|Yo+zxFF)L19Iw|>pN7S^wx&~p#Q*Ri@l={m2#LYR6tx9$g(4ePb1 z^`ASQQ{J+Oq3@$I5-=iS#e* zy_@u!+Hn4cC%HNJaIze&;5w;JvxMK|ciQ`J!faLx|I~qBIoLC|`Y$aw8OpKS>K-$w|F?daq~>;O zM=lvED`5RwN|E#F&BI`p#{Rd>2@;Hqyj7}nUk7K>ine{~bv3Btz1TyIbx$wl#5Mns;bUX+$Hpu8}*y_pzx=7m%`R-;6$=R^j7Fg6`V} z@EcI?(fW-Zle$roR5~^+|1%P{RK7z4T4I-<-tBx?@bjX&;QY4h&+L5{jWKEYkm?eL zzvNln;1j6WFVJT&l;`df=6Kq4YWP=6-ROE;kpAmpiofMA><8YEk$h&qlNjw+su3=> zSNdZX1Od?rq`|wVEI4w8K}QH*7=l!MD40@t@{>&^ghf&8$b6XwcNN42{7U<+ywiuI z?a&ZY=;^WIFt%e_&q1yE8Y<#^FgsHRPGH+HaxWq zkc7?8;!&d~c*SNd8<~;46dNLBe-3A?6+i8hStFE8c+%ai{2iAu-Pp~=Zw2?o38zO8 zNkoXTUA*R6OmvfJ+4?r`r{vbRbC8_gddmt?hxcF7nSg22leq28)BW;&i?T-NjaPpn z{x!da>G6Zppo!2_8MA)-#--X0M;ChIlbd2*3beLsZj@$V-Lhw~@D@fIvoSs%;9Q$3 zL;UI2+xzWrlm(0&8iA`18@?)rsPdK`%99{0FabLo%+7ue~lH_YbW%18o~TX3j8C+bq79jIBOmt zN%?mtb(bK5?{(#~P)M3iOHS7eov}BlytaG&PFXoKFl)q>yjiAyJa z>04fkwh2@t4Ugq%McLy01ETFRSNp<*8)WCvk-(7IMDDGR!X{}raE6h+ePdjg!wgK% z!__58lQKVB*+|rJKr*2aiD8gub-~imK5f3#G(Y_MQ1|yqji>wz+P7)|qe!qROF4to zC{Y5^;7ytwfBReJPy8LG80IDcx_CdTiKozh|I-QcwhxdjKUytRJ@Jb`DP)7wTppkP z3qO8)$**4-hZ_KHFSQq5X1<`BP+nSHWNWQ+%kL5kikFSSsnF%usk=n}=V=TAE<}Y| zgNt#@OcIABv}0+aB)$B458E5wc`JV3mFDy91SBcz4esfUEM)T2%oncGMRf&Kyit7+-Xm%35>@U&I`!P8>&CO?Bf8W|1LbLt*)g;qoZ z6r16s%$1OqhO<%C-=IqQ=KCp}6W(J{=KT8ob>ax^p<47a8!79O^m@dx3NKqyNi{*s zRsLOL6ediUu9%qs2|>faji+l3a!T8uP1lJLO-@q%Mly%9HB;v5uJXsFLD8W0d5{&V zS=c|FUFLXY3RCW-iSDQ{tVt=Uu$;HNX=acmer73n6KJ;~w$|@=eDP-%G{~ne5U(#! zRAs&i>Nckc==hlQw63GQwSC&$Wff~WEi`UCC=~_aKB!?BlRDdY3an|mkhz&iP8@!C z^rN8I(CqELm_e6;3Wp{=MI1t&P50#1hi7tNgZD))&|1$lsEmZnmx8tUGRF=BL&0vM zcVoPqXLYjcmAh|?lkQ6huNHLR91p&$*uATDQvKP^Cs5a44NlW#A6v;GdmJJ~}D(#xor-DQ#o;277G zJ6a+F_W8I|D?e4UawIKm^(FS6+-PMi=2FjVajz}3&c>dy{QF<>iBr)lWoHvRn4O2K zl-T!NiqN$LZ3{tzK355pyf6qWn2p(vPh%Wv&g#Uwg^nC*QR1qBm3b10{AX}R-~Lzp z&FYMOnKzkJeyW}AO^)2KFs7b49am;zUzOKc2r}`%0G#b4py|+W=)CHP^$s48^=6vAI+PP!O zc^X6NSQI8!mBRayl1FKD?w3+kJ*M#I?lp2h5E+ZIGi)Mkh0^YCP9@xYlPZ=8jkQ*O zF>Y3>U2$|r^0(6=6O{Y1`^D48i%BcDMMcWJ;W9taj&5aeRqL?jhm)Y=H z_j6u(@9+1)6s9Y9`H7`lGN#X<%^#242TshyN1tr~`DvtQhPY(UnH~0pHd=QDp=Ft8?&4DBj9(Llu_5nZ6SK!W32LaSVwPaeCd;L%T5$=CCo@}=Z;ef z;s3yYbd-i+mwXthX#JeZ`Qse9iuFp`Ljv#5QDK?6URd30jE9t@U|pkJRDR_jOP6Dz zwb|5}8F6M`0@c%W;V!BqZLc}${`HSvB0o|zd2c_=%sfZI7Cf-9t*8KLIL`-uXp{u^ zjnZF7spJOiNI_G2?cCDjYFwv*n)lBaPkhBAfw#;zWnU0(Ti$Qw#NKsHjCfdk%fo3Z z$b`X_r&nD2_~)f#i`$wJuDT;jATKLI_b4}brLb)R6mR^5ul{u@(c8|p43BeR7#VGe zlbj7A=T=}ePMvrPL6!=O@a~)Wp%qjb{`$OYzdkQO9_F_hfNihzO00q&?Xz~~(x!u(04fHG<{tJ=pV&S^ozP)dq$2 zLm_2pE3G%VkK|3!L+SqyOK0KN)Y}E{I~X`dIC?aU?va9kba!`mN-CvvcS$4NC?QIy zbazT4oq_@?ps;uTKJWg4-Fxo$Jg3j2fHl=iQ28KH6sf8#+td94VI}h?a$lWDC(jA7 zR2q&qA=Jr%v;Mv~Q+^G(N4zHdgVaPiS@HD=G`sru0H+Lt^~6x4{4^iwrR*swmg;03 z>b`N%-EbAIA}*tKxP$%5qY-zuv~L{xI?1&=Q7FyE9Z7y)jG@;Y3%baFLlv`iCBvdU zu6u4@?(tn!WQv6|Dr{RN_tYp+`kk&ST|LYtDMI~hePPv3mB>YrY27c=A%A+`&gB7^ z&Quc3S$tO_qL<;|vWFEhr)lqeWI1^z-#~#ChBbxmGxWAQ#qR^_El})JTL?+l`bZx^ z;%6|8+>mA0Ygorg(M$xMG)s9w_V6kN>FsXI=kiz;h?9B(W?X_;HZz4tqz=V#03pC=3H$J`R-CgpiMmp;sS2eM6=D&D@(FVng3 zWGb7DlEz@C&!>tH*JZTQTDlaW`x~p&sGT!a)235T@%=xK1TQ=ip_l)VVxDiC@<6;- zH2x}I$Bn7}`w5;4T3G{A9pax_IWp^b7*BDzz6y=)blCpUeF54+;PZ_fRT+hGxY~iL zb;5{<$k~=Jwe1|AARb_i$ElXLvHTds8kdub!PWx7UOg0F#^~`|;`h4a)v0B7{PZb^ z1CSQwN_}vRF-;rkSw+>uWB5L?*|U>^N*ov(eF#aArm#gqGx?$wCv`K6)TF+lAesV4 zA@1`z?PEkTG?myU$*PHkgqM>CGpCHI*)=hd8(pxWKO50gU%!@xoDh;$W z&#IV9iqOP{zw|(Zn}z?g;TL^Ky@^-|xX}*yVo_1WK`i2CVPyNB1LZh#gtPtzq^?26 zH}GIrZOa-ydnh0I!vbe|mhKV9W%cp!UP?tmX*UF2oBo$w!gxxjPlYY}gggAU&#I3i z0WziM#^m;WKhm|}k_r1WyKY|cyHP_2)K(t}!r&k+oz$g7IT{C}KaV-T^8Cgodazi~ z^EMBGr3`?GllJ&200Q25$J)e4y?TGMUmwZ-@;}^@O;<zjHCXtW%t<)ns}Tm#U3)pFhzLl^Wi4@au#h(V-$R$Y|+! zsoyA!8lNhbd565*yiNIhvCe;A%dN}EG^0@KrFG=6+0}I-8vg&HHf(c?R!0p zR&e5nDsqc)g^>*pcU9!O4A@0KYBlGvQC|-3GvSd8$}LM~U$qNkR+v&4q`hU&r?=`> zMzYBAn`rVj5J5ExMM@B3pAUZ3T>o+ETW0IW)kUHEtvv0HAmK|K3!+DqpC4n!4 z$$s2&M=_pD_$!DI*%T86<0WGasVn&;eIPMpXfE+3c)CxpHU}!=nToy_2t1 zLh-8HQ^B$64-tr{awc}7h>RMjd~|yiuoEdy8=Wl7zLixZ9}1RC?uc+mk8G0;3UQBp z7E^8i?@I?`?RGBqv+nwUzgBZ-!p0Pt^It8si02A?@e!O%@WE;=B^1DwL@86iF4K4A zO0xN?3!JCBy(_snS@g%P5yw8ZYxN^@Z(kz!+@cg7D>H~PTCsPpAfS^XaMohDT({P! zIuReu=;Jo79_>hOuQVHl<;tGJF}(vZCLAi#P2b;(b*(OGI6AX6AbSGurrs4f9aa|O z9;xc?$|rgvJCpImi9W51ENxjQzwvF|mH105O{|AX3TE2}uPhdHA4vT98pjBXu<_h5$dms4wR_a>Yc&Adf#A zRXoSZn;Cq^&2?OpTvnCRgu3TZ+ak9X=H~9a4>d~%z|u+8B5Z8FaW7Qudf3biDKiO) zXk^(5Gc7DKimegaIOiACThFmBU8y&|+~x%>7X8&@W^xaM$I&~mLrgo}cBJv*h)p{| zG)@%681zPZqvbg5l{wE0CNTfr=sym^E{?W5MpyOGz8p?(J}Wvc2mdQv5?>|}2TFis z>zyK<+rDC z?uxhSgD}fLh-kH<47_(5b4-v?VQlkw*GgSuDYAt#;iosR_Xh7@%Bg#tPxp(EDAF!z+4h4bz;>{&v=vZwM-grHfxD zZd=#FTH}U%*mV5W0@!gHnG)IF!1`yAqkFHE(G@>5a=VG)dhB!g>=bUovXrv+5nKt7 z=Njm|T-zP4i`i9c5{glzHZP?UWmRr#p?|j&(KZ^-w*hE`wTRzUb@-!g#ezPYr)OJboycovYlQ@@Jco4v&G%irDN< z=mEPZFGU?=><1ftl8gr-ZDMlr8iN+25dNZ(p_cxC$Zf~$fo$9OAp}0Xj|0bqvd~243 zJ{Hy53J{K}TdD{SUo1Im*_P_PhjAf`E*Um; zgZ?JtIG?=0IlB*~6{#r#lCg#>nQ&kKFlX22YET6QHjh;5G6rA4N&(Jz70UEeK)1M{ zIko=g_P1IqaeHIZBIq4in8=oo$4iWgY&QpiCPV$sVuW6^NI$K*Cj)0S;wYT0P`~z; z4aSILk=%ol@TpDcreeF3Hp?KkieFP z_T_2<00>9HCEk9dwO&v|nZaS=luq60hTQGXlR1{zwbP|RB6_4z`*HJ^PrCX{u>``Y zFgZCYS1=8f=8clJjpX26A~7j|b3&e_ot2x~lz?CK%@Z?_7Rzc?<-Ee(D2{oTPyeHK z(F7Hbj6T3T&Xvj|H5CUgmGPA02oBS3;uvF;cAac{IhLv45F$piXBc$?Wwb=?W>mwp z_@%oV5|fnN)C8Z4a`h_{KU1SY(A(&F1Y8eR?g1q0X^jM)2)b{hAL1RXGL~>X0@)n? zUY+NwjxxNfG9j3jt3wgdi|@a!>9zVG>uaWOdEXEyNAeDTASX99gsO};CCawvYPch_ zyGO@a+je#}6S{wscT_v=`27hcY6a2GQ{>(HpD!2N`%-+N{fNj-2%#cl9ibe*bf<+b z+kC%>K1#f5Qjn0XCCt%Gv;Lu%TQ`1+(+}TBJmq&xFXHpqHF`r6Yb75hK{m^KSIQsC z=RLfGWqeA>P!cbo`7TqJMUt*+lH$E*)ZEY%6uW;{9>Po^edxHA;5ar^n&(?I= zLqYHiv@CFm$J8!t*zsYhw%t`cGA)gNpu4#oSFnV(p6S7dGYYH^H54pYp4R3j^Bk=6 zm;~ZZ>IX_$rVe=vMkHE0oNY&h+BpXA5*CElnbY&q1*@o}!Ay@d(nC1E)b`jg3>si! zLiTs3?bHOFLe|-RGQX!XDZa*8>dU_^82g3lLfxlk`@K-Zuxgl;nxYh&Bzk}ump-dT zz-(?a+BN^KId`Y`$N9&N@h7GPdM}>03yIc@o&Sd%PPjFVekTsnOqemgO1xJxfoVbK zpwagF0{i&zXAU;O1adk1H@^=}K~?0{UL&6h_aizF;h;SEE^uKU?dQe9uIwL+KLS32 zGxC-J8)CuGchnH8mdSJ%FC{JPocNB@^3aoY^tg`o!GjFT)4H;OJP3(g6iIX z*8KhX&CB1vP=>7l^MqTfLidPey?zs{o=&7e+3o5_U!E8l=k>+{orz+O%2lD`C)6zo zGHItfhR|H_qsv!5q;|fR3>7^*U;T}jU_p^L4gQ+Ck2j;T~w>@ZhIN5Oh?61r%>IriaI5 z+cpv9Jn^6$J0b3Uj_?(Cg6Kn?!N#>dm#pMBFJg#~wE12}NB(REkn{jV%OJXWoBrfi zwmjsqR9YfuV_uNtGjp1L8t&d+^DeRs6I!4t3^vs?d;56C2wlWKjXm|lZ0O|)%DH{gsV1nOy3P)UKv?66C{F23IfAL_iEruP9n1^ZwE z3Blv5UA}Q%NXUhHSxw`M=*Zdud9}$-IVaL=SP$A=LJ|sDmzxQ_6FC32Ov%naUHf7;3X67^z}oy^Ixe zR6SQ>tCM1KcKZe*tcyv#Z687`KsZdpK7Z|9lUy*iDKebgZ6DI#jRnm%){HfzVm8*o zX<3~k-Lrdqe8L(p9pNKlM99*gbwQSM$xFufEna8OyO1CunfV?Va zDp9QIc27k9nzdgk&s+-;9#|d!K_iTrmePu1t|!ej+ipwO@F(RD%b=3CK$P33PLA-Zor#lzXEHIPx^nIVO} z>AT@2CX7+N&Z)}8(^p^^wWyUbZKqg+gp{+$5i~@xI#2RbuK#Aadp(bFscs$;2_mjjgO?XA!>Ry$fCyff(y>*zI4d*1_CKrx^7j@FMY#6l5> z2uz(tNUs(ZP$Bc2^K*?Yr%9CRm)4v^w|~eTLT1b0^J3VPpGB!qJw`@D^>TyadJMHy(lr^8DErWvYnBYr!{;ipsv{lvc%BIreMh}kNcGx84C zn-{i@_1ZJWXf0gYJ~^t7p!~oODygNyJTtWm<&P=hAOO=CV8KSDAFQc5I2Z13#hE3D zE!n?$?f3%qbi$E~DNa7;O*|nx*g^veWj}dL%M&HxN5-Z0^cctbiIxtNaBjJS^Ym%2rvd|CK2(g%HPOW_= zY2-4vPVGG$TPz2Y`BN##dwvPSRfgBi>t`pGRn5VcO$@my>yHGB7#b4{)1-2s64Uo3 zF|jKabj=!63Toxk%Y~U_JS0s((rEk9USW5hn5&p@v8<2XZLWVJUm!VJcT2@ftCzvc zSU_09GK!tp+=MD^?w~c{i@x8WH8KoJ1v&50bz6EX_CR{LMyKA7Y(Bl zAWwC7=Si?8^wQDPx;yrkrN?5YQV%PaQ5Vv&RwEwFX>QNzI2rp<;9ONguN+Z2l!4v) z-gD;TqPHJvoL*s`d)`$hF;%=z+4G$~nG>vZ0N_7s9R>|PoVICm7K(l}q*Jmj>O2ab z4*MWY`Pqfx_J$3o_B8|BPRN`-oyP7P+e&ox!PD*sUIgsPEEN~!XUYEC4-&SZxKvpM zwXZ#mLy<>ozu*?Z`9sehM{iVJY=-hpj-I$|Iq(r_h; z!G%}vg6w2;_mh_V&j!lpU0sPNS`Gw1UW^lm+|7TRt`K}c5Jj)zva7xn!`-CUqLWg~ zs;-2J`Es4jcocJB_myjVv{qqd$xz@@GUB}mkj<17ic3%#<$tW9YgJt#kh!NYCYe*? zuvp#Wyw2mdad(W`dDNMbxc=QwCg z#mhz5GqV=UpOmD31COoO2~xlTVj2UT&^>hHp10iz5FSL1PRmh@OZK=tmk4K%SF_jp zr>i5;Y$MT34ohE@nUqfE)9T>+0|aIjWkZ#P%J7&Rf*9*pw^BGFi?lkp{oxPEBH)zd z!C~~bWRy}Jd1ik`@r>L!EJ09Zr}b9myu{7Zr|7V>lhF+m9btXMBRaFs{vf!_H6o=a z$yE@(VAAeu1xoNnCctpQl}M-{y$yP(zbA4k8}hI;*mY>p4W(Lqu-$OJ@*~Zo8wJ;L zm>s{vmB8DDmXDG2IeR3J=NmKzkEG=I*RYNvHE|fK>%}!c;k}dCp7ZS4wj1#hJV?%A zp&$UlskGKPuhY))f>bh1sEa6>DF8`|uCN)GP7 z@ck$e>z2$+dkSx?|CO$FtSV;X@kvVk-o^JHa;Kp1N!@w0^J0*_qQeE6hKl9g z-#f6`A-|YtJ)IWx{Y^iZKCRgc_NA?Tx4C}z<-X^s#jH!xd&ztM$LxQ&t5capRsaFM z^k{SGZZ%{(e-tn>aw2s~^fU?)_~I|-_X*4Ar|RZ;9m1z=Z>W}*k$juScjsNtIRNHW zMz@Dni5Qta>BJ?tcDl70&{aNF7tZjxr*#45i^16r2kMJ=D+$#%A-NT1S34v>;TWW0 z+C}Ijkj8p#e8?KypunZm`X#;ygvS=j{dlYS_^{Zc1u&1d8WmcmYfSeFhC zm~qzzyH;}Xu&D^ zuVAs8N5fe`8ovDq8SY|w!f`weawgo^Fhy9ix*Uk6AL%U*7G>T!OCyzK8r_GgrzuZp z2VXeRcYc#ztgQ4Xla)J>Cz+MjXLo;YH>X4Q+>|BnPnn=;2UQ*F*yZ%&jE4st)QP&4 z8RRwjx6eGB6_K{mO^+!T6_EGH9@gU_{Uw3PZs4Jej)O?k^N3h3jwfa5l$9?0p42#v zVJSl{PPNtV4B1Vq7X6qnml2jH1`|IMcLw8fEp=A9)?3UC1LN|CDr*W~JU!SHyInlI zeDr6%bcOjvL&Hf3ALE>^8H5V8Wrc;F17<=0I|K~I1a*J)M8W`~lJOspiOzYm#m&ma zMlSi^Ztp)_GP7B}$99Nh19T&7y8*g?$Q^`Ybkodz67ME>T$<-tl?Y1nka}1{C^rmjWt=LQ3gtz8SF-8@W4@8dj2W2z^ad+_6<%2Txe7!V=e}}dpcZPhr)qDb(6o7#N00|3ckj z0(^}-7qHM964t+F1Fw1LHD8J{Qi@;~oqQL-VB)gk@#SK7tKcT(*(yrBwn|-)4vsLa z4G^vjZwm^0T2k|Xg-g0l4&f(}cVeQcBt$=f)g5Y!Yli(J!Y!(XCE46{%}hvfEJ@-cxEB!klzYDj~(_ zrAZUu{7qouI@%{PmvguhvSr49k3@o;_>H`}H}or~)Ull|M!1aXXOq-Vv-F_;xpA=-Z=PCnf8I8Ju49eg_avSZSw*?+=1sFowt zz;G=+_P#y)?$PJp`T%x%#J^0ix83tZ5KL<*6-4(~m3e6okI@rrMwLVgyb2N|W}GL) z1EKCu9Y)`M8F5pcagC<;k#{u7`$|I1LJ+M8){#)sx&=T<)W3;a>u^ZFnYPEk-x8!N zoC!I!=eU&?p8aVv>|+fM+*@p_e36~=EkN> zDas$4T^#1BpOBYLyIz`fi~9Z}XLRv%wy$MMF#5KawIlLLoEs}Y9y=TMj&35k6q#HLHbfJT z)eLRAF!KMsI?DFRmyF!NS({P(JRN*8U;kzk;~f$hb|e9Dc{LKS=d|MC{~HGg`bFD3 zW1?_M8hZOGV?5;o3wmr*kBRPW^|VXIV_*h`&#d63mJjO=3tVfhm;NUp296LBN z8G;MHu+HApwLBfX{DV{Z=gY>^KWC>GjglSq_Oi^a9%3~=UGF!)i~~R!=CAw2k@g}g zP7yx7<2{CcZ?$rrzZkn)a_27(FjhMGJ{u~U-`aIL85=UfsXDeIKjH$pT zjlQM&H@kTyG<(TJqp}$dl@0}qZc&%r%^!ZB+kRMo*_9alwyfxzUj^{-?Ua2As^K_Z zF7vReI_=8*okj|w%>GEz>dJl$74PAneFZgF1mE1d&-%kkYof$;S;-!i^B9CMDsNKh3JL|kTBx~PUSg33d zKp)TrH>AeF`zQPdGbw&<E~Vt7 zogJYd{Qb@I8;Tjvzu!V!ja*S(PnPdq-}e5r6aiYUPrlrh($W1;3rKt5oKs25b+egY^MgZ$hp9lc^P2U1`c7%zb zC`yI#Is*k6t$6tW<#5ATlS(Cdy)%#g(SuguLM0-9PzB8yx^B`M&FX(eYLB-&p;cej=uX*5SI$@C+NF3Dz583LFhRm8K&G z!cwAlbTM+B{l&j~`&uIhgo`t9R!Sb$AAB^_bG%~8ctl*AHKs>{Ii%?6bgU+=@;ip$ zfp2agJ0W6jGHaAjSLE^)c5&K{R$rM!taJ4T;BsI&F$SFdP? zgU9*D@D(G1#xi=6SjE%gki-If&!S!&-w*3N73rLRY{s;hFBN%K^5z3{esW-VKxk5T z;=x6E_HQnVJ}ZpAHaf#VvCrfG1b2i7O4pZ%foQuGDvr4D82ho?wb6qB{ymblOiaj> zk!j|VgEy24hy=CrjEGHLCR3QFQm#>4myZ1K(po06dpHWu`>-Y7!*Raa6D;?XXQ_(1 zRu9|#hUYNNoc2oYnK5<$kmBZh_hr0iA+&1a&sNES4%CL_VO3;ivz58EaK$6dNww2z z&6k?0H?v}S8}=UZc|(ZRUGcwvMU=|q7*>U0G?+ikj}pRCcV$jk;?k6gVF19d&ju4I zOMV5vh;p?w46FOt1n$k=W8qI`NOEsZ7+$;6MO-;5J@edSSR3)$9gg}A?!Ez2vwT?V z%#+eO)q6rJ?mYYUmd$%%fh5B&_mT1!Ue#Jj-pyCPhS4=Y0bz;ij7Sx&_?en1fsD`V zx)Ls-&0-t`gElxD#||2luQLAW>JYl#RI@z%7P4bSIC68 zyAA@i%#1D|Bdt_!e8WI15-By#Shg@=_(Wj&(>1=u1f)_pr9o<_2p5KU6mG+QOmg{J za}K>4>f_a>Q6u|A{isbKm&4HgT+@2Aonw1v-2!hbz@!r+X>46sxEf+!bDb;N{O2J> z8MwMS9dp3c>F0aOj(yK1`dd}jKR?g!0BNj;$`p`ZXmv6lT%e|el2d@1i0cQdZ4-?! zzbn&Uesb2DGYiL-E(nvc_h)qvJN8wXmxK|99JBSdW=-xsVpPBe3q{vJm)V;a8uAN$x+7YUh7Hi0hLBEIoevKEVQM> zhw2nVGQvO`gE`%XiUx{b{m?Uo=(sBd14|#Rt5jEt1J&1fj9L(RFSO)*N07vJjmQ+~ zwV78X1kUom<=8j|Y?%GtXpSEpv+D^pR40(RvSb;ZkEf@m3R+K8oBlW{3YNH@DJn87 zCwP3#IIX0`WnqUw5Xtv9W%)7=&uN%N zT#9MAb0--}e`DwEWD)Z4$p{(8u@F>$r^^|YB%ciESBV`u7Ks|LtJ8C_=6tiZe=Inu z6rh`7^yE6|k@qkV8#09lC~Huo&P!YWmQA)8D+u~9EJRK46;)R>@wj^A59bn6DRK4; zuKPW7%SvL<^&j!4E~TbeoSgh@$!9_!_(I2=RZYm9HIv4j(P}n$)BB#!Ux|4+yu3BNrm)dy`AFdlIP&?%s>c)z&5)T%AXWv!tTrb=jdW0DMqv}<_@$r&5Hp|bw-x-sE z(}o@E>pl~#RKVRX5W!wS2sj80-=~7G*g}3!ZSIjMkDD{Vr%-?tz zmpnT1ltjik34)`%!6byR>cUM*pJSKli z%QKX%dM*sGJ&Di6V5Ga@-8;FtAis6cfk3Y`tkNGP)kj1z$}L~=jmi0hMvF#~F^L^w zO@jdpyh*Kahg7W2Y`{JLsp z88xTz!};1aDhxVlI5+Qi^ZBh z5?srdT(N{ar(*hH>5Mwf`k5A4W~>*7=i+pek_bV9*Ba+zeUi*#Y~U68cg=R6jRM>T z1Z=J~HsxZ7@nAOEa&^T2kUL8Ry;V>^8`e?-9LN!#Y&sT}1U)+LVu-4?wX?;$M%oYX z4_OF4<@MJ5?F6UC%E-d-o$?`Pop?b$tu)jjoWl2*z@yYOCB5k!vbD;;_vVb5(Ft2b z*J_12bev;R=i-3}uP7s68kbAs zTz7`&0RRyF=iQ@Fy47XH_N%R(kT@)@--)my_*EVe1infqHSko zwb)c=o$IgWugI~Ix2OnGCL;KQ{p&vFkxtOuPW>iUxsSfuzj^@~SY@sxs% z2dk5`E6*QWh4l=DPKU9+#xOaOuOU~cd2>q&5Xr=B{ytM4?>u10$h!DSpmq;OfedjU!1bW6|z;TFan>= z?vov|3{q>#&a+lFjolux1`s}4D(s|+Tce~994?r>jyz7cS(_M-6P_SAkX+_iI~K4f zSXRQ%LQKk5x`Pnb7vbZ@)3lW3s>Ww0^$l--opnP<4ma!gQK*o=CzvFSnt5qk67l|1 zWn|VcdA^H9`Yg14@U&Y`q(1~yasU{_H7~$3_p`w7*d<70y zI`+h9&$~|?;1SP**K9{sMZ0ltpMGI9G0nmAG#kDUG-i7k)pX2>o332fxrO7hEZA)7 zI5epjAgO%>`-6kTwjNjA=4?5h)=je`%2T-XD}9(J9`ijeny3ckZx*MBjDpD8XMF+y zop{u_&X1Tx-p^{D~eZL@(Az^vc&AKpWO>m&3zWroJM%y+_nm;2@mB%F=FI0+BRdKeo3e z4Nox0t7;^iDscEn5+jjKk45=&TK?|tRl0Ha)On`cvC;^OT?O)e;B`q#|57_VOX>8Y z@lw2g^^iPn%dAF+pf?fHN2ljc9!bCP+A;4W*K?KO!xrIsp}+#hp7FJPG84z-J6#1H z0DE(@y7MRM!?&F;gx@FlLGPxvXypkGAHMd9UX~e+<`T^>%1W7zbS~o6^+q(iG&9<_ zg=+Nn!n9I_tS)Il5A>`jAYpBcO`(`f261ZMZ)Tpf62y3yYWYfyo>8Kwty^}NNk&%p z7+j)kjf%3uSS)K8sTq&vc#74GxGF*u0R?en=HsPgz;o4_UA>C$ z9|$4o!$TE+PH&{-Z*=JQ=UdF-|(;5_enrFdKc86lDA9w+~+?@Y&JT zt|sDXq$WWYVj0(VCtpf-x?UEWfMEKrtdvk!TEQk#9qaJZl=tH==0x&Uej@Nd0qQ`{ z>;CZ3Z5o6i0+g7qw4M}97{N)Y#fU{$ieL~TFQn-V@Jb}Ytkd~T}J z&bhPDDrWzb#R>wm{u(l1diuqFZKS|^{nYTRU7M?Zapgd~`*UH`&mZ!BS}i6RgL0B>!m?zJdIs>#Qm`vybYYp$GskSBu%TBt0_6G=1cJr8G*5|nQOv96wj1O zaV$&KuYAb2OKbVpO&Ij_nUifx5835PM5H8MJJ^b($Rzuhe+V%v`Lx}={WOmcyQbiY zBS=LqfVt^KmeJ zs1^Xkl*_CUL=L(MkNZV5HZrVHg54t&CW$CWtpeZ`R9=nOn3RYh> z7p-)@)G8*FLheZAjp(`#w3^mrBLSQ`gPb)EbrUtq4^Kie2QO+a^+Ml=yX5AbeUq@t zomlgZBPwBcvTZlbC|XNklQ5yWdZ%@!UbR$$94+0wUE!{MeAw+jtSFFBVW9~dNZzhLaY4g zO#b|Hk8{#590AcA9IIlje$dVoUxx|NDb*L#o7v(LTR%3`N0!wR*7ZHRrdU2g-H$Dd z0%6g3c0Xt_6-BR$&hE1Sl8m?OU-rJR%qW=fj#LE+Ik1y#>@Pd|&L`lc!&D|*Mi&Wl zcYY+LB6BiU;q4-cQgRqsI?TYNU}Tg!bv}5EklB4t|AR_wD?RR*O-Azrs`@Q(Ttfux zaI=>w?rZ9kFmMxC*p|gqS#Y8mD_qjHr3h@!y+OEOaxr?`^sy~?3elITR zU$BqV+$6c?ug~6zUA)hLl-P6WjID%%jRWpoTw75GX-^s+7I7f zq9mKdiAbjQtpB&1+U}45B(0+G&ptTD#kRtGi)3kJnWVrkYo4+(qU%aMP_?}2p2b;= z(oz7Bj!}w@WeSI~5M|N}$8uLH^^>biB)Bfy#)KD9nGr$fX`;rW4QRu~3#y;EDEP&h zwk#WU{5{STmCasVE8-xe8^LFA*W2#jGEII<4lnU^b*KRTJJ?WjRn_?-&EMX3NNk1X zzCvz&ke58Bl!5^dXtdx>T37?ZL_}rPMhr$Yhed7fUo?0bU54~C<;oAVp zKj<85^wm}e!CQTj%**^VpC`=lXVRq~*ip(?xE~>a^<W7PG^)u0C;$yUo4!Q}6 z31vL?Oo++<6i(gr!8LH*d2;74&20chCvyxK9B1EYqE1FLSt!VPFJq_PrAg()r?!-i zUY2yW{S4)`#pK+wn_|4mSSb26c{7TRenu$yL=>@D9pZF^Q{w66-rC`b_TZ?LG4|9f zQeYoa05T>LKYl_V5fx3EZ4Ew-oo^465jx$ynq`^%BqVORvM`FfF;E!tg3Mw^JKHpW zOktV(_0CAnVDwA*T#Aaq8;g}+PK=IwlK(#DE&j?#WysO=2Tn0tsp;t@#YNzj}Q@HOoMFo!77^cpQ z^Vj3HPnjA!V(1MT93DT*Rs=`WWXn>>11%Rnhj&BRDbn@>G&%<=c+7+X&K#cY(XAah zD0nE%)uI=_`m__K4Lh;~ni3D$={pUdy$pUs_t{4NtV%P->-I>SUki8^t{o%QGi=P+ zEUdLCSk6TuIL(%m>dW^fs^|M79U_Jl`>I2d)XDv|rKL{86OCNi=-Py9A@(cIpFeZE z?c)S#Au;i5Ag#pI@cK?QcDz?_8n?DgiBUIQowI$Fbk+wVrMBkoaUFi@Zd&P3C>BQ8 zHUq8_)qlF0%n+tL2m_JnI1P)m!UxS{8!_qW0V#$6c3UI9B!3pC0)VNS|9}FAeBFj) zX#kfE7mBB-^_#1rTu)BV(NvrqOSxN~PFUGytVRc&iI)A^lXWLWF}o9*$+g|6T3PBR zUNGi2?0uZ3$KK1yQ%}~M>lt?}oBQiogM&;A1D4SYyiKDi_I*4lK#E?Ka~0Aq0G%xD zOkZj3kEL|@Dc-UM!=3edL-2};2s2c=ri7^AMU+YmP{YTYZL)&_VDLlKNk`$7;SX(L zp-q{TQW{@-ay>`3lsO3P*e?aD2nv`td-(A=-cGy)tLXzkdxJ0Nz@Cqaxfu3Gg(lfH2;`{LZQBn z(m&)5lMy7kvauj)9b?DCLp<3~Y;klU0_+-Q!D_1~fY%le5C_5NpzM=#Q)+UGQVNmB zSn*Mogf2YxbUC&REc-;eQA*AgM4YzC%T=)PYJOpnc2dza{Sh%(H!Vsp;8*zu(kL2p$+c(j{TTXbm2G zcT9*83Sc`S$5^f#O0RDDa5Ki&%m-o`k_#A|6PdX!@uY%Qx9Mo*lZrZ6lX`7vqP?HO4B4}j{j=Y-W7v6Xf&{^yaP z)6Rf=Du~$A)FtN#k1qPC`Kq-}mj*zP3~H8Gg@sD(g>Xny@~KMsLtx%|}!+iFGNqYtN~!#;)BBk0A@m;}kW8w-bBrKqri9sV#>T&Ld2xRS@LIVCa8 zh`*w`voP-XcLzT}l%A1|;vE|&FquHFNFM%!rA)Hc;`rZP7rV`+BaP|$X@(;WnSzRGE-`Wco@vE=myL4(up77=S zJo_LsRMcbegXQ%57wRN-#1Zt-3J?4z5538=(nRuwR5rRfEX2_Z?ko4aeFd70Nku)lS+ zQD{!=T>a?Su!4^aOI6v

        Q8714V&al#Voms_y-q>>Oo-thaon9)>9&OQ?Q0ZnR~w z{x1T+bPFXZP%u?MetKk9i4kXP_BfT&#HEbtwT;X$+csITQJcC}DJ;+IaRo*-m)Gsj zwh$rTGhI1)fF9#L%-V??gi*>SQc^nkgNuNjXeW_-Ad}IdOa~`!5{95(fW*W|-{h7J zF4RGApT`mHxvJDxQ`2`0^3wk-o37rAI$aJ^K9cBhO#CWJf*|uV|ErOM$UUA9%qd)B zauE3X?|uaIZ@NzS#j5)~S72%8w6&n+H@Bs*SSY|R@L!4bFd3Ap`(F;-VnpN!-uFb# z43ioyI8azMI)|PViWlA3JM8TF2&ynBXe5b2Ktc_x`TRW_rZQighdE(8z*;}vo1XKg zg29A;GB40{XLtII;;a7IrjI){uV&qq3x3Z(6JVLt#@bM`g%f!_95x4-Yfx#b>|^}3 zN0!Zf9A!!93*ng_$82O4#(R_0-Thh>_a-TC)-!o`N{AX%NZKeTOr~53+o&*SE{} zavkR|^cXMzc^6D#N@6HdDYQ2~erEkEOOCBTnbj-$iiKQgtw;QyMo+gBm#qCr)mO0No9JhTQRN3S~m-2acH zv+!&3?ZWs*4j5e{9c*-W2s*mEOX&vbP?3hwEsb=8fHX*hbV-NO4Js0f#5;cP^AGI# z+`H~`pXc1y`5yc8O{4pr7e;b=L-sowj5PWPDdNwx*VZovB?=y++$q?-Qt9!EHQ73< zd=S!~CFci3<#Gt1dnrtp4_``}kAAzu7q(zpG5dr!xZWL?5~dt!1m7wzN8=2KV&?bK znvW_5^@9(j`eMjsBjipOQc7WU1ZSKsr<$tHV~W;H1n^VRPr>G^@~=O|;tpje=gdt9 zGfBQx6G2iPiU!MB&wXH@QeV%%K@@TT@chj7vf><_=bHfJ7e;pvySwGj*KV6$KES>( zFD4%yAcnzc)Nk=TInX~UGa~W@N2q*UQ1nsYsB`Yu#KRASdHV&qeZtI*pUtlHG#=(- zuNJy*_iulHTKsiyaIZhTf%dSQ!ZFlE<}bekvgvXkyBL%F{wTS90^p3Q>=Kmt+_Iwl z1Z?#}UhXL>CGiao`A>2^`GpvT2GK4xjk@=qe)4Z6^R?$rCG65;Htfl7c z;Bt2H9sj@n{My?vZ_U5ox`v2i2T!)YjH%a5S80fe;eJ0qO!OflWxE<))+WGO=oqaj z9OxRaMAggxpOxHeOI6jMDBO+o|Kjh#Bg-Ea^T2K|bUF@y55xrl(AD^E%EMN*<(`)_ zdKtf-#zSyeAtfEoVC0<&vt6__-H1+4&Qzy;bm0tartF@LFRhsq$ zZg;q|ZZMNUQEpoXFm+hP{KQkdO3>Z63Doh~2e&M{UEXZ$r^e279vghf>=hKVBra7i zkw)8c_3U61yU~?(AZ-Mi+lrI~J?(L4cdhqi2D5(a~PuGKgq=dDFG@d?-IzWTyYsy&{V|nB!FVL zkA$)n6{9{Mc=$`Qar3z(?2X|dXn(R$1sebjLw+n&YeFg4nb${<5zfR1m+d3Yz?*l5 zu{GBOu6|1^FEOcbUU!1m0*_5x=hOc`ja4d(Ld8#(%GRv`i z^$11hRgrB10F*uddrJ?hoz~j16T@1kG^@OGPWqmHU?G?2SI#VcetK3~R_DTvAQ0L! zoT54l3STB49W1?AgE^_m5H4gK&Ty2Ufw{a&jvUU}kc(mJEGsv0w3xP-ZSl1K=igT{ z=kVuB2GjFPUB+!68s7_Ga1(#gsZ~(&iQY*99a8v1esG|R;hCZQw*P;=b+9{}t#ET+ zz$%)e6F_yETV-oIh^AeIo!C0tS$%jKpN*FAmD|B?kbaB&XJE^d7cD3I;-0 zO4>0ec>y&vO0PIG+;oXOai$pnOTeYzU z;ZT#bOr$;}9j|7Lq;Aaun_+i9JXJ|W8|6qtlO_s2bm&U_TmoDrHGrqbLLRh7{&B(0 zg-E~b8hCL?-HK8(p>x5-;hyN846K$XV$$mEO48plQxgA1D_}}Q3azXrIlm6j&3o-s zfh7t7ITo@-#9l|ThQ;=FP~7FxyzHs|)3Iy!jyqnAAw2G7Apbrvu(p1vYh%sxu_)Zs<=AJsKeC>hY>ff{ zN;;^l{o}*7<@abVcCYy8^3nx?XDmj=q4waF23aH}C|VW^$b^Y8MNW^3!)T~swiH-B zUn)WRiJS$JWbS) z`-k^R=PGRFjgG0=l?1a7ip}2z)q(RWu z4p+j&3w_K|SSd&d@UTMHMad8qUJ3;q$yO6@`IpT3d+%9(SBTH=Y%~z6sRdTAqx)wL zsKtCpA1yXrPm!n$Jm=QqX>)EJrWX+rW}bdPa=J0L=lYf+ne(`RE_i<#fJ0Es#tX2mtzV?wm*PVxnB_PiYCT zWs-(}pvw!(|CDCw=>=?6Z3u?3c?L3u=G$UfnsJand%?#tEJk?rYLFX$oCx8=M);8L zmSfcZ#Jl@;E+%b|)zZ~K>di;5hudh<)N!1_^og%hR~kPa?qZPHDM!-TiJrdw;8e|6 z01Balb9YPurj}D%=YE$GVotN+jDzSU5f^r={GGfj=Q|}@<%Fh93iHIIj8<(2d1NHS zvUF%o7_%4t{MC9>6Ms9^apjq?wq(lVc=XhNvn^BmL*t~whSL3mrpm*betNS8&3*g9 z0@Jp~yNp+wjK+o-r~r@728K)5vJ#ry3qG7jM?X9ir~oiXKxD2}&*YxnJ-4k&46j7p zS^@O)5vM=DSJFQ&+eD|Jr0tRG#dAxc1oB6aT1Kp?V}jDVdYayfcNXR33(U zHw3_*2?HZcLERGiBhB#p!(B)E-DYy$ZCr(D3dSccpdDMLa|Hp|sA-{NlmoYQJntoi8(PFZ zwA|a_QpEB7dC%isO^iTqwKN?$p)u!2&}x7ffrNKdj+}6B&=Tn@XY=@+kn$SsU_@z% z8WI3tCR=8}+J7Ta`lDm*uIpmlbZ#ohoF_{+Yn zr_ZyZ*21CUNK0ll_RCk*c1sl(%Z6Lp13K-tR~YKx)Fz<-7JvLuY2s$ZVHrU?byzdC zqrH)ArEq19f3=$p8Di5L3#=%$LMM9@ar)H`SF_45Fj1J##D0;9f-&0pK=)@?s@phNuT)Sog>WH^|*|Em&#=%j8#U z?*!3GF_fTN6vdIb5Ga~~oRvHB?$0>zv`?LUPe<1bHYv4knGKANWexc(Lc^pzrjoer zc9oOx1Ao0!ew3V31p1DYi3$#FgIG}25z5+16y;zEF7^hkMAjaV#X^|vU-;2e24|GV z<^lqer5XSYI{_J(L=4$qO*BPR6EB4vL|99+66TG@BKXPTWF0GFY56hJm5V1U2|aaI zOFGTT*?k0UyJ!->=xbLLi~AauTd3x4rJSg{PQze~Brao%KI|YM0~4g6*{(KgyOG&@ zFbY!iz7%w;C~LWHBNUJ0UH6o(MKddfT+sONd*H(n%q$G?m5<*TqnO`8#Z$ZSm}_%D zxmDB8CXfrOwkx060Y_iIBF2f$+eTxY#yG?dZ7OXAM`dY8^J9y6V`!r*uS7#10Gr}w5f!}bsdx7OYYp9q-~k7Z1Z5w$G@ z2Eg~Ewx37wrtP{UP2jTHc#bdge4q(KwHt4UoE$b3JC=%a6;W}=%^hVr3u0`5BX}r*~|1f3l-LT1AB}9RP~b^ zjyUsr8DnL7g_5Z|gOs)gd9H7NbYFI#jLs_jOQKg!EKnl!V)A>#TYF~ymp`mKp|Jq~ zz{*w!MM01ae4%qnz#c_D>h=VK^y5oH!!7BESsERtbV|`_(DdJ;`MamsSEqrj8P8@4 z84|Q!J=6?!MUs~X7<3GokZGEt2QY6tzN~3s*9}-9W@~ z=bEU0_agu6jH3*a(bnH^V5NR72vo7acOZ&slM9AtSokeraI@NiHJ?>c?Y=h8)Ccxh zC@KBZNti#aG)}7VQ{f~()EUhRtl8l|g*2Uv}n+zWK9L_Vspit`nblgi>E?nb>E889Jm- zig?Lz>pi-?f7avs&R+!sO{U=-ME(3 z8tUeGF5fGGq%c;n?5hC0TFyS>GypVH&jju$&if=@Dl{&_CG|OaeVJ|nca0Kok;a6f zCHY7GQRR)I9qoX@n>O8}DQz+5k%qvWX#K?r93i<%4l6P`dLcHhSyW;xmApYzyi5Va zfgqybagUPY3_^P@u{sh;e9HysFvi&ff_grnCV1HahUvM>6QhK2KSOOo zZ7`FaYXduO%38kq&&tgLa7HmYBhaQT%F+1;lsU_VRj`Y&F=a)AKdo1I^k&_OX+BKv ze4{HhEHXb+`1qX^Pq3V`zfw!#1Wd%~_fOV?aVUS5ZWhq;8c3EyRwea&&`df5G zGOi(N|K-CSWtVG5WqZqw(tOK}mx7D_LN%vXZLXgn004nXv4e4AKe$kPq!LKxMKheD zaZVqmIDLN`bAg8~5N^nx8BCwWAfXNpk<&=P)X$$$s5_b%kc*ss5UkHM zC`Xe%Vyp|lfU_b@F zhSUz@W0XS#p{X+(eZ`>ybL2Zk(e?o&1ywxd>dgJFp19ceB1@~n-xbjCS>S^oT+Sx? zvF?8-{CM6OvHNcGMCazAC)V6V*nY+M)|M)PX6@r5KS16-Iaw1Itq5MG+@tk00iTB5 z|LJg6$#F_U8B^FsY;6TYU)u%O@aZS`-J+bnQaqa8?yE8r%({{RKJ{ooux@g+CWysQ zP}j20n(PzT(1k(6URPSBGkR^P!r7rVWcqMT!tglw?RN>5%d7HlKe}`t{yyA@o(a4I z2zlWswPI4?5oJS{)NBY^{fnRv|^8ESYQGn_#WD-`*{A3NA4GlWcFAST9Hdveb zmw4kOHdlC~tUyA^_~QksALR|en211-Vxk&YyfPp8d&%n6HxhgZ=Tl%^k;JAXz~}1CRL)TzctNg zV`97wvYa68EM{_TcFt!3z+0pDpo0U~ADzfOqqSmh*fVe(yA7#kvC(PrSNd$xGU?9V z!o7{k54ZCWW9(zB{jgjOp{XSX$sSU58VOLvyKj7kJwCeh;E2ue2$^APe$%$R2z_!?JP%yUDiA$IOSBF zb;duETbeF!g$Z$AtBkEtH)7#=->X1~MUC4io!+Vks9(fmewaT5Wc&gXn@ox_PWUzD zvd~gIO3prbmPydC0|(M2C`J9rf|*eXBi(|Nx9?{?dSzT}29Bf6VYNqQtp@(eEz4!a zi0|>k{khqKH&s%!qa!)+aYclSlV9dDHNQ6uV!lXXG+14r82_n>s@@J(z>SU?k)i?& zkSdSTiT%uQ;PeDrWek2wHJIFmufqkuE=m9SLBDqpUZV})Z7_OCy=dAQHMn8T4OY8Am~0N&d1 z63hMi?g^Xfu1<8IKV!-gpRj)*2$ILwyx@E|-3xzs*ku~K{(e`9Q6UH5X2e6oSPEbq z@_3ZoTO0qmto<1BW0|m z>n+vM>Rgw3l}wz`7k3gSdwJGP9t1CxNPqo6M(7?PM zB6>JM6dM50RPbDIig2&)c#vkT;$O9FA`9km6QAslt}r(FF?&g#FOq~W475WEY$=Q$ zC6^Q%BEmM8?Z>Mt09aX++yfw&%H&;&RTBG%4=2Kqg@*gVUq11ctA3yoIlv7uy#@4X zZFuuFz=yD{Dz%%(gdnGHGJY+Zu&MielP{~vRII63j}0JxeiF1|iv?ZdUC+rn`I}OQ z+uHvZ_oFBY{vpYCl|^qs(c?~PtaZj>#lFHY9pmOdw+{TnF?=RJJMZ>BX?$%e3MrXs z%B1<_O<=fFd;(xOq#-eCdBt?K$%~p$>WKiaE7B4W{+ zw7MtQmeXK21!Yzlca;uvqKOYG-= z2>znr;5{b2r%H%3aVAF;1qbp)B0$*%KB9OFsay|6-k(zBs?rho$zVyL1C#0Us!Neu zvgDWYr>gOzR%bt?gRojUKCd_$1wI@GDTjP66{jV8;U8N#fC*K^YCq#74BCmrWa(u{ z3f*yu43{pMNL3B`#V4h&C)O~07$AeM=NRT$rI4VPAj)GHp0Cmjz%76cKk@EsO-C8z zhmGD*OCLK>jW>Vd|CX9*_nXYv{ToFPgNV)Y(dI}Ac z25KOH;0^`v%!+*-zc{#!H^fqqeJNkI&HNW0kEm>1#2K#)r8L%SWx@CqJ0BI9X1g4q zD=)yXq85vU*tb(1T#*%vvNRS@YiS;0B_P98)QsPE|K<4k>4b#3kbmk=?r>(qgV4yM ze17R_iz z*f)#0I@U=t%PR(?rj&MRSWVhkek3qc-ITI372>{HNPv#D27WA7t>dM(0+dlQU9*9C<2t&jZne9d6 z66F%r9Mpmes>{4nJN3cGPYKz7r6HnV>Cy{6U7_ERjDR(Ne?-!FpX^^{Pz2-Q5ts!+>?kOF>xa*iY-nzd)P%)}>liN{H_wH@*gnXend4 z%p|He4x4_?v)jy#R83q89~eiPg!K4c8wXx%j(~Rkbs5 znu{RDIv<`URrU}B@bZcdQ4|n1J#VeNV(yF||7NH&`5OIz+4IU^Tzl?Z=h@f8KdGrh zSRu&RY!msCGOgM0>_iY4{ngI6EvNW&aWzR#Y$=AT92{{jlK_>)h__V@|0ou{$=KMm zbW-Q1>RuEDKM(d}lFYy$=j3)W8e7y~G~zb;@bGYH*yHjK0FH6wEj*E?q{hV!)+#Ws zp;VK%&cFJo+;3C;Y&I=Gm;L1*REIyCU6Gd6@UhvovT7_`fRr(kM`gTAV=0A~|IO#M z!dr%Rw9UxpIH_pMNDF9+oo$41P-dbj8BAQueA9TfwXVYWWy$E*Sv4(I(x;tyfXBVM z5(S*2o6%9_dxuakwzGjz4#Qt^yB=-?9i%wqz6Vp-Gvh}sar1|)#CWcDzaQ1M7X^~K zcb+Z`0d#BLbB%dhdQCn7`yLV~IPE?Zkc(RD!H0dz_omwHehYQ&*k|FIK`=gkkp_Dd6R100>X|Ec+q?sS|0NR@vG9%hDVra}C1V0{c z51!H39WNsYJ^43UBa=kwqvMHh0;0hOAw{ZC_nr6xOq9fa84A7*3GsNjC@A5Kq=Fg!oZ*hmZ3QaH zulqWg17wn8uq|}`jnH6d>|VPWZ}MTcE$?5PD5SK>n7t8h{4F5ra<{nIJV)tp7cFe2 z;CEE6BM$&V-`;0GuR71|?Ak8n!|(4_5kap&-e!v@nt=F!?=Yj#=}z8I9R$@jE}O)K_F2njMt< zy?(nK)q^IBo+qSwAtk}~ijncVM$0V;Je@V{1R&;CEa2=dOUL*+xAm&l`fe4o_fc}F zg_9*!#B1*fQ{AC{ZNdEfOQHf4wQ$2H?;S6G#S%Vx$UN8oHC?<*#0kwwYDDlsiP6a_ z*)aXYs+*tQf4_+6ygQ1~;c9twHY^Br3kkI(?bry3WMqgGP^v&o%AnEV)h8jZl4v!JUc#rYvi1!@$9~N6kCOWV zLUSh>idpS}s$n|1nD-M7ohWghp(xXo$p;lF;CFk!&YlLM?vTl^9X7PgEE^v}F z6!ht`QuMsUsv_0ZcWvcA7u&|CC`7VdSEzS{@atUhy!2FHEVm(yNiCTCu}SmZlOE!_ zfGLQjFd#3R5XlA&PLzDX!bVcaOQ%O(NQ_xPB*^xpQP)JlK_l#6T-T3#)?l;$u2@eA z&~%98Q&xKbskCstVNHhYY=x(ae`AVDCdu?NSk|3`4PC0^AQC!^4f#(x<$Q z8t?A2dO##8zgkzI*s3o=&J_=(l$+k@pRx_C_lWRzQ6;@(!|9$*{c0?)Y@}0b9U(Kv zI9=M7`I3RGoIdg4d96e?Ps;=CDLdbu0e!Ftq z{&erQd`H>pB~8=cNF7gm8yZf>ReB6HeDn*3@0l<^gL%5sT zCTbTaeRURc%fV0!!c9A&Kao}oBZ)Wx6}P2(9a8W_gv~HQqtr(Yz&c4zKkq6%&^St1 z+Mmj(+#!k9jsK=IU{DdE=@L*v1pI`mg9=yr`h}A?5`xpvq)O7?)p4FbdAPH2(IbCx zQ6w9WDYz_JW%5Qf?3%s!P?`Lp8USPf0YsIAR=*O^$!o2{6Gtl*chnAI7s6L1P=muZ zbS-|%Zc@+c@lOc^$l_@eR9ntrG~xZ>kT3H8({w?Ri{1Hk4R(XgcK72OQ_lSzCCaUz z4*^NycrrjpJY}rr#^OnJG*WOA2;mRr)~YbZCZ47qNqf zb8qx3&n6n~{Fwar z)r7&?K;s^fWv1~F<(?aUyCg2^27Eo0$p6-+$5vZU$c6#D`z>zW>rU})l2_8uZ-F6z z*C&}njx3@sG_xAK9vK-<-Pz}DC-q}P!H6Df#tA2tIX8Bd;Dr2;stZiCbX(-AiD9EcY_uumEf#NHBW?K{oy!$%zh%}Q-0-&uY|+n?NT)MB z!O13B!|GvMkS!XKenls!&0u!)D*H(Javt{~fjc05b5e0i6uCS~u1+o##bj-#GU~@% zF(+Oe7?*9M=jl8deDM7~*{CxgOPIsUD;)`xCW1eUY8Y0xcl=A~%{AaaKINlIOj}Yy znx#T;5(U})gqr(Z+Atp*6PSG|mn>1#cN<+apjqo>b~hlUArbSkbCLGYE@$f9lvq`X z5Z(JMC$3m)`@H&CLD@ko`OMB-wqc_qF|W)G@GeJ1o*Y z?EuxW7g_2l=rBVvEn*KcHoSvFG=bgUruA(d@(JRQOr%K?K9x4kfFQ-CwK{UEfKcs_ zw^8Ir$I{(%;4$6{)_GkoXgbdaw>Q^%w~bSw|1O@w-jbbaYnG6;kq97s2EdGfs#vz> ze%t-rAW{V$XfNwFI=H%sm`%I1w6hc;_!_+D)uBq20It-1qK$!*Pvp5V{Fc3e0&^U= z0&mPar1^bP9!fe^Nbh%j6Z%Pjt>r{abnmwZv;9{uFM(Ivi$BzNnym|crWnM2{dchL zfP-Dch7y4k{4!;Vmtb@#h)oS;!STfLfWeBb2;72)YuR&L|6yiEH-07lhJOatD)LsM zuj3OzHqsmsh3m>w(8GzTuM73z&(U=RXi3l@vc*l+grN(B0d&zX>Z={Cm%jH2(NDTP{hCLq1&T3@zkH`&C>- zliAUe?1{yP+lH-InKu|jSf9%0Qt4e)bY5WRhltU977mK_6XMslHawvj)bv)1z#Wg* zq>_ggvgcWsp1re8g+q5g;Q?~(oeN4kk=3-}Mh-f9TwapaeM+jw8XQ{2rz-?5_X=GK zzr7jRRwsw3cMYqFIJ-w{U7TL;r6mp@$=HPr23xTlGzePjXsxMZPFTEi*sJmWk1n?l z{`E(+AJuKMrN;u9hQJV5hCfi018*QdWl<0xeoyn(m{-wD@6J{LdSY?DtB$e zGzPzD6yL;z+!_ouvMKxijs5`NSF1hPmw|_{Rp!QbAt&&|Osjc_3q zn%cK+#yebg%J$q)@VErTInud2XyOv#o4c7uT2$%~6&%Pw;+daE>6;d*X^BYjsK!R0 zZ03Z8>Tx)I%s7$`rO~r@?+lH%l0g&3L*E*}jz?r3h)I~#ac}${HA_0f$6jTA;8tPZ z8j}6}x$roPO@RGpYSkOH_*y%xJ%b#r{nfIYkE7^6Ju!U^y73lgfK*;_8tfXm{~fGg zV36oAs;7`$XU}jR)Gv#@41455D0voH6^9bI35=p~{{y{Ipp!jSTHy2lnlcP?9F+g| zy;!hex;fm!>pMxnjBxuJ+xrbQV*0=q)KNqz^6cnMn+tX$WdyIbt61{iLi-nT>_>7* zQec&r+#Evb_u?D3l|&Ta{o~`7*_L$Ha6k2Mo0=I}Ej=lgJO06$?Q2g)JjPV-W;3++q*vCyKzU(+mNJY*?-s(9x;A z&}Q&jVW5(op#U{A` zP(E~$4VCa#6wr_#b9^5~B=M^VWZovTBJejojQNUFx50}=h@BYEIpwX&$d7Iyx+X$k z%a%XxxD`BCP&=CEUNNd3ZD*s_j;-HK6{HtC`O{1Rn*fxe61!c_8kNp|4`{+aW-~@i z23v%Q{-m2IXQ@H)=-L6#5>VMKiu+kbiQ^d-X!(Jy{#20;gR&!Cw|dZ&Z(k1@z@(0m zxsJYIo%SYi31BR1&ON7muoo_+vW{b6-&ot*0Y)f4TeYVvPf;w*lH)vQk`yYUKJ9cr zu`rU&p(pWKpx){}{S^4`T072S^UA*Tl?uE(U^*8#tg2a=mF9B5C;MyjQw7AN(uU3< zDOMh2J<^L5h+y-x-8n*hW&0(a%KBd*h%|s_czGO$t3?a16KLNnKdy5r`&6PoAz@d+?x>;Fg?g3|# zbbOH{ow?+jYRm^Qx2O{F$7`PBAAMgD5U4wp#MhKH6~E2%t`l+pu17EBHpN&(SpBZq zzvNAsN=|O$G9$w~YhMO|lhnBh5|Vw{5?fi=0J47D zja7$LaUNpXjA^Mw(P*YqYtu9t3wLxnEY>{(N>k+?kUkD4#9)wy`bWZi2ovGZR<#|m0F z&*1TA;bwb(POa1>huI(YP`w;YB;5s9H2^$RphG=3*2ACIBNw7q=sf$9dblu{h+J?W znsvdkbHPZe9DDp!)p`&aJhL#wEgh;|fHp5Ym*je}{9+PbL=qpdR_(JcAYc`*%(bQT z;n#VfOY~|;VQ2ztMxC{n>BMEdvO?_G!}~aW`<{1palBKqc<8PRF_|WQHh85Td?rnv zPesQoD-~3~Gwi%iT&FuCcsrjuNt7EaEzr*~| zWm8sqxQ9-AD|szt%u7W9gO|0n*>2sfYC&hzf-&1 zeiv+I3R+h}NzP@YpQ$j+I=fdc96o23U(UWmukOXW8&BVwH-GAWnI+WqJ{8l1S}v0X z6s$0POwfl2o%~auVV>LAR=nY2&06dpDuq2#eV845EVGxRT8J20y-7 z#(1xiY%(Z&Ep-!72NNIJ(lp=BUIMWk_6CzUUZ|BkZb%rvl%F#~S~M-vINWtM5@rb? z4sAN;7ri%}+lh_6a4b$KyN2Tgq-y# z9Y^0px_0^GDbY{PkH<(_sVF)gmqwzTO&!RbmH^YtIb72%zS7n*3L`#_?$FV&k)PI_ zS61V@@LVa6RTR^yf_gh(@A07c^YXby$IZ)$Bh0)qHi|^>%Nzja$1z-K1Q$lM670nb z0+2ZjgAu>g8!GP#>Pr=5sW24_cRLv_Oa7L2AuVqHg#l=)VikhU$88!}mGW8iuUDCW z*U8kSN}VCn(U@w?lWk_*(O>d=snBHrDaX|A)TqANiZj0(SKs(B=jf^b(%aLT(mjiq znU&Y*HalcHPZP3ky;&yama25`a)JAiQU3&<$lLBd)y8~09yU?vJ<&lTAYGk-9cUgb zLxcrCMQzsC!PW&Z6p^I-i855(-wHnw2*3h)D(7FE8k?r@_P{5&gwO!;iEe)tJ~v}f z%WWRyGay`TvScfy{BSiR;}nC%JvbAZV2vW2*TxGL6KKi1nz#NF2 zlkLe0K1HV^Znf9_P@sUN@I0zmn!N~Q<24E;h4~;xldhNH&3@hIxGuL*vx{7{&6Lf1sa=pdGMJsajgH_I zNGdUdRL^f-geE^nZ?)i_0sOD|Zzi(Z@z^H9->;s|4%e(1)1uW;7-p#3&QmBa9(e>j zXp?u1J>0!9$>%q++w=I|>6VxW_b7dNJJ=|mq4v9fMv$`tP>j-1eFUE zuIju2QAlMIrn@5MSa{rKRsI+&Eb5wj$=3!u^{iW4l9kw)AJm|iTYFoPD`4g1AI05} zPu63t{(`+Zp}wwOF_GWGzm&*jR<@CJ(s_8vwfW$p(QTf5xH#U~nNXwAJy)@_S{3&RkV_oL*N!9WGT5NhQB`gW}I5UUML1gfVH zpb{*X56efjvGxj76SvylM>*cUQzJ5#QOsaO&Vv7nANvFycnr&DH30U1BC(8dTvuxv zH0h)A^?$}W#|&`gu$32{o3<~uEjTLEy_PcmiWwo+QAykNn}BG!ul`#@r-Yn{A(3Ve zz+JDz4e3@%6+c)gWrjkf+iIY$W4gOjBj(2(rHC?ZQm5q#w!feQ8?%*Uwny=ZBT!)S zELo?uck}Jn0gOEWpfEEvMh^`R8MW>pNd~7$>^~x%CE|Lv9P>610nm&q9!CV`RD%&K z>sR^#Vmbe!)d~r%GO22a4eD!3jEC^{0HB(q(ST`JYkk>hd0yR{Hm-d6e89xR+9X7zCtmwTXfZLTnf02MJL`5})Ar zMUSX2KV)n95;9gwaZPkffuDB2TA52R>Rb;<(!->tmuI_-`{apdm|A;CC?EA_%)XJi zEB;7c-{E7iEXKmmu(vT^bgu$Bzh6DOI@S2x2&9M#M7@rd&j9-Nj}=?nw~TO#s?00m zl%4?_vUdnu6;ysKthQ)R__YMI?k7@KKduy;DlXp;1QOQNo2dS+2-Qse zWZ6xp=`RI%sw7*C=k`p@{%OvapD9s%U%8Xq;?P9d3Iw5?9#qGWulSjq715d1C$H=N zL0LZ6`Vw(-mT$xZOTvQ)e7cm)g>s0$tS{h3l3MG%%$%QZoTvP@bNxEsH}K~2@|V-X zU7)pXmtQcyjl`akR^Y|>nH(p>BJ$79qvXED13e#85+Z<+9daH+28s>`aw1SI=duha z6l8+bPv4Ozc=F5hmt|9`T_4trUM^~4Jufhj;&mM z$yz1CYa|it3d{VFnX#tmPDbXv% z>(e&044?4wq~0vO&pKEx)#W>q^3v})3>~sg)MTm5TTeUX(K8XVZUkL{ps@?i3&?`dA#IYPmH^9IVfPJ3=FBu6zH( z?BOnOnkzZcR#bm6&wcR|>suj;at0tChC02dvR)Mm{}{SeoVSZlW`rBtC@oxGFkie^ z@HGkoKm<7x6@(Lkm1krfcU_WE#HS zSZrXf;pl^s(#R!adfVI7$fbm}wc(Y!i=wFhGi97@`R6jRgZ9$(*RLKv`=8rEEyb(D z2v7_rwP#cnzV{R!R7wBZXGJ$;5+5bE8w<>q7ytn2c{LBpcTp4^EJ;$->;ivaJPZX% z%T~nTfY&7xLf5)yG@(Jb(>F$0`?Q&y!~M)$#!Hv)-DQ=F>d$&HTy%yf7Fv&igoY64 zT%zV1wWGa-F+x5}y7!sG>Q`3%G_k~+!d{=_E`;{JewX}0e|yUF{ng!7f`L!>DSG$A zLCcLK<(G6KH zSalEU(H;&xS>MTfGuL}!A@oshE6Z)1sqDZj*u%ri3#jNE+_L7ogziWPr|O8eP-FO`{o|7OVZ!4ypSvFgTwgPF`7~ZNM1cC;l~cJ)u-ewYbS*7n$H)$a8fZK}?l` zREU)Cjh&aRZ032IwStUl*Wr)k3u*RoUGdt;KLc@Y-{0IviCaARE^q{-8mqJ2)2?`% zWU|eGDTDWUY#=kr_~1olvY5YhnXS=toDX+qX{Myk_)C)XAqDOmG3}of6w&0wQT`X1 zpa}DLKs0ewDqNuzF2XD1MyrB}9?m^vT^)d_U`4KKr3ilSs5lgZWMO5A4w@P9%q&sd zIEmc9dpXT6oV@n6jSZ`i7YJ`+3^=?SVIRttZ6u8M^AE zQ6}#1<-B8K&*brhcSkh+{-uwS+d;+3|F^N~vY~Q0R2IDS|14Osz$%pw+{+?Nl4tez zYd}>n_m4ydqH{JYBGH}3Xz?0j=3=90`SQg-(|bNc>~AySF!Xh`7rpxx5)InF!n-Ci zC}EqJ6X%DdS$v!%siXe$mhZi{qn|Po>xU7kyu$n7Ni}4}?PjlMe-q<0HIEts(w_Qo zRqDUDf63l$sA13S^^YE!g50%8Gj+!M@@>yL3?LON1G0p$@=1qN>tgMJAa$QDvu6Cf zX~9Gm&Z6=L!)Ea>NNpC1%o?nP@xZHdmc(H*WgKupHTu3FsbEBW6UG%E(fB%`m+V@3 zulHAd4$5rM(FPooO_NCR*I^Sx8&zn#O?2;F&RH(9P}tSza&(b?z)jn{ofe8+S{ho( zQvd?MANc?STzUULSQ(169w`U;rj?L+RIfgk5e)VDpciFR4AVuyMQ>@T7WS^*6TvKK#e6L)ZdToD!?sUooauR&l zcS9w0uS}NP(~TIkQUkq9Zz4^<<#RLWV>DhH){7Lt5i(J#?3tH)||-@QH@`+#Abv zdJoy!Q8E5iQ@%)TtA>_BRWF=*yvs@M79r-3N~K3I{P^KFd_K#OQqI2!_RE_p=+E2^ zsVL_e9O-+JO#fr)Dj1?{o9@!JbV$ciyL5Lg-Q6YKC=G&2cc*lBrxHqov~+_s2na|D z2>UJ1`|VG-_sm=~XJ*dvsqD5Do?R8O?)kXyadLOo8SOZ z^1I`+vWTte=d$sB48K5b2S|sb23M@2;W2P*hePgKE?x%C%lZF`$lsZ_%I;&RSYWr1 zV-8nL?!>^Pa3gwtEYL(oLI*(gIe64flp7RrndDRMKI^~JUe4D$c-0k5A$CR)=EqRn zja$moBrT4LPHU;TkgudDptY(E-fR52uWRs*aEAPkBL)Asb9X^etAAMTqxiwt5{v}~ zvp2VCfy$*#;z|@@K+@^>v#iLMVih8Z#E_&7OX!Z(xn}s#5ovX{x9km%YB9t3BRhvS zgNwnWdL{LE4oSH7)vsT8;DdE~g?+rVo)Y32t?zN#!is9i1D}G$jH3uLhZhSPY zvgvkt9zS^s3L3pMP3!JZWaqoNn)m}W3Z0awAs(Fqw?uVkoQeOU0wCgrF%((?LKOSa zuhHT141Ss$d&QG+-#Q_Reb}Xy=ROA0S-&9mkcgmUIRQ(Ag$ByR$ve5U!J=gQoI0+z zD=BTZSCik0%W=6GDH7o&+=&o zA=Z*3lay{Ff*DcjYNsy3$?&W9-WpSou?jlP($OK3vZeVj@F%hrjUL!vW#zRlijdzq zpZuOOlHuFY-5Yi9a~l%5;HZI*B=%{kS5H7d$Sr3zdpGBI&=T7UKSCnnG2~#*BjlDt8Jt@2r4(sL^KMbU4;Dyz8bHrz@+d7gaz1n@u ze$9LbmlgA_LcTHt2HIU8ml?I7VKwuSIUU~Vuv+}!mJqLM*+G9Hf6dt{q7UjS-R$ZD z>;Gs-ngUgXUZ0%&yBoRobnxYNhBULazI&gX!$W7UH_~(E>j{FTRvf(}E0pTf3>D4c zhwtBmknIUxKHN4C!A<)m%Sjn4D;@x4q)^h3C@tXfLFU=;Tm2kp=MC+#A__@BN9AQ~ z6g^QC`OirTY3gEu^s^Kp8e1lUginUK?CG+H)OZf5Nhv8%Qg!NMl^|hGM>~Uei-Z3Z ze5xF%R`)8}N9!}n@ULjp$Kt-y9nODF`Yk2|Zgt&7&=u(Y{h{HV_p|>EKyOSUP*NJ*V__=o(M3{ze-o3L=u5tc&kuckL z>^6SbU$w0^LRzFrHocqiQEe;7c`0k@;TgfRabl#E^-Y?E@fYLC265-k zoGw;R*O~vk3-)mkWVK$B9DyOr2wWrW5{*~~jyPY0%z~X>n`{K<$@{0_KX1>)*E7VM z(eVLzB&f>+d3wreY>tRR$uicJ9yssAD9ym=-!H83g{3TsPL|o7YskkZ_Qd&oT{91< zQmYn)#7V69TML2L#LVNeV<;#m+GrwFgex=6*X2BaB8=g z(NTadUoi&qRJI2ijsrSdFqR2o6uIljm}y2@j4rnZ4X~W;BLyy-CBij|O%tA{H)b#s z1W8)xf}QqwH>u67dQr;WaSHU?@HmV3h64O76zPHPQlck;a+8N+MRbxopZ&99#6us{ zP98TB3K~0vk|JOIs1Csr3XuzxLOgr7Doz`?rAZ`UP@uD%f8at}aL2s|y*0$7-*<*CG62)f#okqc7#gPA(&RY(oYpq4QbFviD)e zodUo@+`4{@J77SF;TTU1pm0xG4LXF!>{>r_#8$%(VwSUY>5pe=90Tf27?&v|#ihQj z@~IH<`QVZ0ly#lj`h4*Y=d6-#86D3(b>Z(ApmRav{yr-$tK~bHvT*@;B!wk4#!SU! z%<-lJCrR~Rleyjd^9H(pE{o)A=SQ5=6lTropCMhm$36GSJU{>{@4%=cw<8`%ZRf5F z!pqne_ldDW8EJ*q+0Q#&PTs6KY5?ld7fx3x!Z9ZSKV0CU9P3!7A+m1Y`9+5w!x%f1 zn&CA$o@jYVdI~Cx7N8(6{hGuQ0lxl>GuV&IakWLu{j-M?#`)^f=3;wfzx(X@;8eE! zBmCF~ZaE!?P!50-nuKn;N0s+*CKv@@=|w}c&#?lYp}lsCS8=5}5{g@FbvTkB>lV6%N5({u^-Zw0 z_5;|1p3DS&Jq~|K03x!Vm1-yNVTGaSZ%4^R0WiMaD25NeP+n*_cp>oMe0TSdtKYTx zp6ie=X`dQ^8KtisF}2NihMryZC!*=o&OJ%=FI_ejMKv<_Q?AEqB z@cx|VYv(V@$uY;R*>2cCiQB!99%=qr5-8+BneNaFRW+trQgyHW61msv=+|SThgs_j zXG>;aSHlGMgQu|mjAB(#9n5N;I8}4=1u;{xJ_#9TItBQUD2BB#u>Odw+122 z>8L;0G5hUBq~olHb>$L><;`dM z01o~gm};c?pTo*6Rwf`VZ15bM8A7srr&TGou&t2onfP28HYY)Bm2$@k=Cn7)$Cm&j zAsVtR?_hxm{QKomxvWA$aQA*2h`)#bkNZ#0&QQI=>euEvV&1G}4)@%Y|yt2KHd z(DPne1|!?byM^ZT6CM?!}8(u`7rSTQ;ZM`|$G+#S}ZcAxaBfmMl#Rm3O@ zj^nUO1fs^;rqWu&-tZt41u{QZ2ysAZP_3VmGMa(T*+&OUc`4ya;jgJgrk0SlA?Yk} zwN&{VhuF+AWL6U37kiPnY40Z{bmp`L9tmbA(WAE6;|0RxDWB~W+-~LUR>l2QNe0CE z>1m;!KWJ@t`Yu57QiwI!tAAnFK?o42`r+#RI8SY6L&YUuL%y7g)4!|dmHijUttQZ! z%gMocIave6_5*-50L2{&Z}q+V{K~8?;Jh5;p_jEZlQp4q5sFJgzm-cN**x$$cE`kLfa zW~b$2;}oBzdH9omO(x}6T~t*vBkKlfqT=-@FjVoz4WE!B@SteZx~u2oq@qxQ@lz>% zijPQm;0vkMqKXcA?Gd=&&+8X|GgBQ;tT&ohc!5m@i=b|&>?4UV4e1If4)}EjV4hGkw-0)FLCk}7)Gd3 zN6|&v9esI2dGgQUF8Oi&8Hy5 zAV`5xu^pi;0e6k%uxb8)%?N2+N^>FJwJ!Fb;RVNE-Tjap-*p?JgzI}&v{cFr6Z!6w z)>32)fs9WZf*iNwoGcK*~g);oMi$bos4(R1L{a3Ct z?$*$JVSjqQd0Xoy>tSUx2e1Lob#TcjU2Cb)xR|WqlHOcS*~ISfffu|8jzdt?gS+if z_ZmXrC5g4Utgs%)0@<`rn$(8DGWy<3jy{b=&MgY=9I{AZ0*1_94TmEV1LVRO`SHiYCotCqm@0C*{(_ks5xDfQ?$?55=Q3&aGpbsL|5N`5vv$io(F z`}#i4Yy~%sjy4EDNfdS&&E4NC1L2;`hxR8j9n<4+8%6Ep<0eL6sf*8>6yXcY;NDot zf}w4tUdcFF*{LQStKAT&EGBXMOFz5^~hXI3_GCLP#blfUW2xVZt^YD zw+p#xKJCfje@XE(>0jO@b*@n0;RGe1UpAl z?X9T;M8}rP+tQ}JCE*F<0amvX#fTfvlniZ9B(8D>#s8{fSs>D35Bu^_!d^b}N^w7T zMsqd$C+Q>QTgeY^rVd@_{9&+R5OacsGe(mCa0Lj!^lsBlF~(4q4c8d!4LzY*a0n@O z;V79AA4gZ*hqy4~sDvnPxQpVSBWce0Z_N&4i={7++ettWP=8S$Z8mlideL1%;TL@g z*6252`citJ18q7_lRZ6Vfo==)-OeC0$>4}cV$MY{2EDbkp*cQ39tes73dv(BOe)cp zDxi4F$}X*(?r3*l?9;9t^-Wuv-m*pOTHT0+-}?t`d;b@FC^vYoHu>xa_rxKB^C$;R zZSv+0b;tDc&pAm(Ko9JuuKH#`-&NojA(ve~d0sie)K|GN9LLt3Et9 z3oDA%`4j~8A+98d4g(-y5mfSN4sOj9rjRQ4XDU&st))+rqfaCxy7HEiF5;On0~Ldm zo3)_R%E^9^YzaM;($pb8l{pC_4xVqf_~&+nT!Nc?i%g{SZW2^PCa?#%g)bs6T(u)G zyD=3lK6iaY^k_yxhc~UXKH!cZBGA|CzxLr*0__4|7B0&Nv^Z8=2EtccxWP0vsZ1%rGr?UY*$O$xyn-m5+U7wwSOAW+uwbVmE+q1`y z?zrez>TdgcCp2pV*D|G*#+|daH@BuEMb9ov>B?rOd-kr|59X&+9mNw+3Nn1Bd&|oz zeEBxJ+bEPE6lhvWJJ5CwX@*WtJo1&{Y}T~cP*N0FGaos(o>#PkR@*=sn&z2sC~~why3J-?KwO2Y1Vpy+!hc&0|0m1!vp9s|NRG~Pmz=3h8sM-k2a(( zDTGtOiuDg7? z+AY=ZlbiSON01#CoJJhxBmhQ(TOoCdbkS(3UcH!bldZ1jVOjU*|$Qcg<^3oG5 z@PPo47Jy92%m8T1od)XqRyHECiYD8mEhH0Q&#@C5l2Iu-pkJB%t4k}gj=OF#Wt_xL z{lOrRc&}){(t5f7yY<)ImqRR@R&dabF&S_r<=#@wk32m{A6M$uG!;1DvAbNA~yc&fXq zhKK2M#M75183I)$>5%zY=ibY#UblzhIsF1Gq%8WfK2NDvA+-dE#XO8-R3!BN26V(R zF;N6$PINI0T7WEK%oJT5l-)@B)_lBkS=Qk#Kjqr6q zl$e-AE2IX})P4H;!Ub=E zP|9U@91At?u{*0iDGLAqIzj}+B&Z!BWh8EtZa@?+VXrUB*7b{|uZ3tXo7-_~f5n*~ z<6+L}$;FIHT>e$H>ew();Dh@vc|^yfz|r=s=GJD&Om5gT?Ou{##trXM(y1DN5c@CY zT@5V!9sjo ziXOo@7RfeVz64d?xcD+cTs2vIY$pwNUmMp<_y&XaHqdCSMl$8=m78<8SOPLm3(J^m zgiro$c-$AAV6=|qVdsKMzi4ewdoDfnOe{T0hIJJS2ck0?QizhgpSsY#Q((7NzQHhm zi#{#hTYoA|G)R_badZl$cEo*w+)g4w)l00B0<=a*;qxwJR5v8Ji>(EMN#*W~_^5JC zmm+ffA7h1Lc59alwcn7G^#+?FJ8+dR4zu$5EFW!Gf(J(qq0qd%=)wP1x!pYL-0VY_x_#h|_kO89S85jK>XdLcQDxd!;+>2k8jN^VoEz3x%pPjy`d^EY5 zxGu62|Mq#(BchV(LWw9cNu8f+h&i#T>xjnUH@ss8Ag5L)j(_tLdj=kI=sfF8Da6>t zg)b^5E4K+HpCU)_(F(=9Kn^~(d7}+q!J+Qe9y6i@(r=*->A|UB@8GnGok6&RSD>OR zA{ZTCeWVsz9GbAV3&5BphJKv}SF-uc=o<0MO8PnGa{ENYW}_nt)^da0QAkVrc~1>D zbdvd}I`j{gtYmJZfmb}`u`M*dEbhTPEEKX4%i8-Wm^up&$H zWJWUty*`H0u5=(ur*u1{13Y07LO3U=e2=4^A%c61aU6|q$Ue!^D(K07p-&=85;ULNOFE1T{z!aDo{kefG zvtKu7Q&uso-zOwbR^B~qBtHSy>cn_?d5g0l|My5HfKgbs7;A_>#bHvq!?n7mO6L7Z zp0K3avVHcS6i&$nB_$pFFER&1P4kQCZ>T;Q2r&c~rs~s!w44_X7X;}yJ5Jx4MR$=E zjUN7?9-A0!cR}Y9OOG_f;c)8wg{r}Ebf9zh{2JI@>(_ZUf{|EtrR|ZSy&>T-7h`Md z#WLQf*9Rc|-}8VU2>1?nXhAoT9a)A8$;-uS!5dcpKKB=P2`IH)Xlt~E@gd75X)J>u9lm4i&&0Wo738y&6T_{5qu)K-W%=v0tq#1~r5~;Q_nT<7 zgx6mj6n;8`HnTAuj1i^F6JIH)IvWLPZ=u`<01z;ka(56@)l;R+jP38mmRy-+2(I-d zEQx0 zlBD~!M!0=KzGXmL_s=ifS0;a9&!RB^ywq5Du>OLdJnWz<+N}zRy^SGYSz>f3OII2o zP@@~Bs>)Ot9TkzaB2Ai01O3JKtq&?9l47#*$4Nf^MoX=h`c#L`{To%WJ7Y{JG(A2z zj7dltm%D^8f!KNZWP~BTW>lqb1R~Q!(`@+0QMGC{PWji`_e@U_cBce8P;J@?SH|M(@N3dy0NBfT}M-Ki48t4&FeV{`vUddg>QhN%y4$jHHM&`v^7 z4EjZgJUvySn+gI-ojvSk4HWM>B-V12C@Y(OC{d*f75x2U?Djxsq%N%D>V8Oozp?)& z{Z$7fc^^Z^eK!4@jmH>TD10-mxmhuomC#(-j76dfIh4+slTszEQUPMa&M+&UT)Xf) z!!C)dxr!LRlUwa2#e#z#|0Gla(S z?)j%rGV=7_NnRQG^m3kQA}*f8_OE`9a~_1b>95RXQma~ION0NioOum>sb$FDMg{9r5-Eh&$z@=#EOY95M=S1&iQ8h2(>sJ$)BqiPCeP<{Uq8_SXg_ABO6aF4 zm6`IOA@+GHi|;#NRI^ryBy>BT8vQXL+V|EM(EIesn}u72ZhPnF=>X4eW>d8;_DYA(s~7msXHL! z%)T&g7oUvgFMVtliA}v)GX~dzH7$h-rQ*jzr0$&&pD#xP9RqpaA1!(jc#B^k_Zvtz zYY2~3DouMQa65d&iu_v-4mtlmry}^CVPdZXNHJ4dSyi;Jk_0Wg@khB2kBZcT)-Z?; zfk#<&?Q2TFuM(_S-a0a2F<#3hgS{%y)H5D70nKj-IgzcU1@wwN5M<;G|6nZh<$YFR{PIxnrhHuo|pF0--5 zma?TbEi@7PJ~@wNJ>TYbsA;69vqZf4dzs*U$y$CKQx^3+sdyOpesO~BLgpi;qqXCrXxNkCRuKZ!b{S;46^nH<1Wh{m>W49pFfH z0o33dnn2SiFq}h=Lu=B(gI6Tc`4G?+oX|u^?m!+0OQ1%_qTG;eT9w(!CQ^`+3fd+v zJ~M{`gMoX>p(=c~KhAS_2(zjTip|~TCJ<~ILWi#E8??SIm$`J&VQ1wDtG+C)j~i=Y6XV;wGFTA%W6OY29zIfvzh;v zn{|ke_+e9#u3_uQiZs3Nsi}kjnwKTP*wrC2Dq)WcKw~=*9V1v0C2FowO*d0{GOj~r zu#!UfqR74J{d*0DnEbyun1ReK;wks4(z3b6DrNS`lcF>uN3>SMPYVCDDKyi4@fqni zty262=LuxD+2KO+rlZ!3))#a@4B=4^lI|pc%|H~YyG-$PIYSXr8lLRp8*A)L>%wI@ zM$nUNs@T!y_H1%^0tK5F*$S7Dz94SWLs1%kL!p592dngg#@>#|uVs6~A4K%}q-^O_ zWTGM-wNA7+I?m0h*B4>gfC?CFr)^zC=tA8w8Kcd!J}a4s0EfWn8!BQnQcEm-RJ2dq zQfO-M@X!!ug7D;;m1Zj)0NW^Z0f{I~gj|Gc+j*&2I&2sO-;=vAPLI|Ji-;{su+$r% zR<ej6i~7=%{^;a2HPHU_F^^wBIQ7dz;@ep94e5^y z4S#8*5CwTAo|=0CpVSRD9T-e!jQBx5M(8U_3GiCvVr@JN5zpcUawj+l8QSnHM?>ox z80;hC5GvC>LZ9d}&lXpq4y6ARW zH%0!FAm_&jJ8Rn28f6}(J|46b)Mm%G5{)U06)mE_YLD^Y3Z*ajed z52D2nq_({ZyyNVoxxd_{jr?+riTcLw{uTf9DB!zu8vJGi4mluQmbxt59)8O-hI9eG zXDH{&2+wk+eIDiXHgL=7qa+C$Mav-GJyX)kqC-?X8r-r1SRo!*l|~6(RRI(Mc(ihf ze&Ejr7xfJ!Eq^>IQir5pwcyd#YWll+*&IawL2~?($^(sYp^2g{)`U7US$2xoHJHgb z=%%O95r#NPwJ{QxR|}OycmipezB+@lfw2Zy9hE)l&a=a=H^^t^CK-zZECg|H#Cy7r zo#o%}XoK34KGbrIC-d=vidm4;1)toW8mg|>f8cxUL|V!x5_lQg$l=QMYr5dZp$5$= zS~v(r7;vrmbmSODg*wq_1Q=g=;@$n8&0pF5@}?@?@4(TEyb@X%WNf5uR7{>d6;YhB z7_3`MJPFjo)43FK3i@vS>XGDYsog6+Ug7Bb^(A#fHE;a3f;_!^u8z# zfU%s%;aI*Dk5wls4n+L;N?@#Iaf8Zxy$?o424jqB{OS2r`z!r-s`vXIIVFTL#Z?R8L5CE%X_o^=&W7cnaMcI%rtY$y6Eb0AGiZJuZZeU75XBXB@U|{rS0UKuCGTTXA|Q* zG2g5oDkA$^K!#X;e&SN1C(y{B4F{5{97*CO4I$@UZNVi!oT+Snz90JW`i}S?>>Cbp zKHS8*zBmYn~AN0pW19*N#p}>5ab-KbNzicTiQVLdnqNzi139ZN@x)JuE37* z`8%vOQN6N?@B=q=n*5^F##HxQ3h^GfeqmD+2=a7fMfCclTMq`9625&jaBG*6gJ7hxSwddZ%oeSa3KFgZC~}nX&}UOQnu?+hQIat z>4U+V+1FS{@*Yi|;v7FvvduVuX3)VJx#jGuMbH`(Mll$w;4ssN7QhH$iv))Je^P-5=^rsuO z#N($=998nKQJtgH88n6nx*j&qeZ7JZl29^A37Sw)bYgJ={?UU27g7q&_MgnU9zUK0 zzGHu2D1aqHR7Q9r$AdnL|Fc5j%Lxt^{V6EK8G>45V(h%NW%BpG%Q(z9&~LsaQei1m zmQYqygb<9_y#*^5v5KTZv{O?OAkWgLIPOg<5}FFsASw7Y=mgmPYT=I)K{V1`V5HwS zv$NH6WfL#_;gW$5uCV!E7iT-o*oZ|c*Qa4JpnB4Dw+EzwTy7LJOs)$=G_3KpUag{s zV$8Nf55Y$pA-K+&mZfs|Pb{oJSSE8opucU10V*1nS5Rr!pZHs;zO)!b%=xcr+$-oR zSdcfQRtIy~dq91IjBu`7sxe1P5(!VexJw*Xz}nV)mx@XVCMf;y3D9YEB=`AV2O3a`Z% z(U{@08eO=(4*7T|3qVj{SKlO+>HQGipbQdLxY%r@2(S+tMr-yGtJuf`Aifm}3~?fE zx)TM;QTV8F!CjuVHK_*ecN`sQ_UxzM8T{>-KK`KnYL8x`W(x8l;)N`M7+x5m0awk5 z#%&U4r(Y({DRa36Hlvj3h@xGO7aoa{tHyGF88(8_f{hYeVzhO{WM;bsK;+^iRwkun z&viVClr?j z^#wxsV*3c+*b(_^*2kPq*q8O^pdHut8oKak-qpLrTXnGD+0WR#QRd>h9}c|rlWb2=f>(8#TC76&(u5?lrD7iY~=Lf8oA-m8~5<2<4AY=Mw9 zRldj9>&E?>*$ZT4vw;4vtdM%-jbqIJ`5YM)|B8-iQHL2;CT(fOCNpEN|I?iGSVgT( zn}alHWm`C3RU=**<4G(t2%pmZOiEPpoQI?pSh}s46ICCB>Cy64gf0SXMrpG6s*Fe= zbs9y%%oOqK+NrrKW0c)I3S; zUDm&X&0D8Zs;*7Gk+VIT6cp2Qll@-N2UGSM&Od!rPJnp189&!kM&H+Hqk-R;1xPX@fXNphe^jum+PuVs4!HiO;U`RIau&2Z@(ySv9-e)KonbQ*>q&K$&?}Zd ziM-FL#g?!W8MtiS9vT4ill$srY@a(V8+~xbZ+8RyCX`|C;t4aA@G&~HAE3j@sEDx4 z`wEKMemB%q=axX&qP+}5WOHZsWR$Qf{VT^~S&c3Cv!zQql*YJqF4oOsL;x}8?%Gny z1wLGtQQwfv;L>CJz+yXHVKG!y1E$-ze-tN9_Qd67T|SA|V%al)SlTaAgg2Ar9fPj4uW_n! zK-`V3Z5!lekf|YrtEop|>&P@wMtJ?Tb_ao-<#FYRS|IGkHlgwNILx?^O75;}zKs5w zSUL<)7D;-KfdGi&+R={ZMjoAL*Nm`6#Uvp?@~;ehvJUG_iZ9rcvB<_0W)BE_c836n z@^}0C@sjxn5%z^e@{bP=(g)ET(`hyFlF4>2|8P|<$Fbm!TFqjO9oN~CQ4xQmfx#TJ zrhdY_io(c>)UP|nJxRxo9LU&MRNcNgCIJ8pg#errTiSvb$g#yqyYU!jof0vC_bFn4 z{ie~1OHTHUnn}69dIwFMet{N`b|c03+7FeVJ_>wcL~k#*GnG{9d3t518DZfBP*eFH z9CC+e9$SQ+*qWhFLH_#KA*2*1N|De#7FyRW`-!I+HD2E)`hA+-W<_l3UU#sQBm4&(7P880K&dSlwYbbmI}92QNf&tVd2Ap zWzPwBEO7Vk^{?-a3(cr(3G@`gU65z8?2M==kmoT+b(#@+nNfq+=>M#o@63&$Y;@9e z1-QbVqK1X%6VGS#*uqH|2mt7aaXj;jKc9F9@SnfIa|l95`wt_BzMGJwc9P;@rq{1Z zS${0pil4(C@UdYsn`s}3MIXML7^1Y^QR7-iSTir6%6h&)PCAUhg=e+nlqih_BbS+QSfOM;GTNp1n$n{&b?Yz(1LZ zmtGn;fq!Ye0O0H(0Mq`uA6d}ig~UFd=-}J;LFtIgpX>0sJbwOKir*~=1%G*KL*5b{ zoG@O0JTUcs|0nD$b;yhXwqm3n2eu56TE(q2OXO24cuFiMLI40Tfu<-d^_=cz@vL&* z7MX4n3SjZ5FJ3%%&BWp&+Hfihc2#?_LC^X}2<3|)4Uw|AnEbC{g#NlmETN@H;@A~% z_MsBvX6HKNCPSnXKj>`VgHo?gr^4h*hTYres^Z*i*v$*%jKZYNc~?9BvX-(RjRKdPOmR}F%G;YSFqn%qbU3i3zV6r8 z90IyCK7*vk%J>SOc2vm_8~}c2K&)cC5e!m@(Pp?2=HRn*E>Op8^91%(ZR;%vTd)&C zLUTj1F}#_^tbr+R6@1JDpzX9b{*`LlIpza0s&q!ZK+Y*l+gWiX2bVyX9bw!UMmWA-y%7VunHIMNjKRy2V`a)x!Rl~+593X*VmKJXJem>&hF2iOvSrYpEe6Fg;ir= zJ-XWAw8XyuN+cI$l?f`k^W2cBYgsQW0w9cBf1?C{tQxY6nMuyeB0EmPqzc*CK+iTs zp}*cPz%-SW0?7*#MB0uD^^%ch4T96P*RSpgQ=-PgF$Z{wgE{-HUU?1(rpH9P^L69} zOLwdg_rR7T3A-qN-kw%~NiDeiWchPWaS5IePZd7nNdeNZdl`OXA-UJx9R>B~;aGUK zMrODF^Uk5kK#0tDSH_;0v<`tPWI)3ZIW4#=Ve0anJ^=`wEDorEmf3S z=liI3S=f((9b*_~Jv|L1oNPO5W9$dBIaCD(JGav;rBiw8T%1A_>V?vh56qQm z{XCEXH9*-JEdbK!h$PoVvu8K&5T9chxQYm3@(56Vi>wUL4AQwBa7DhY?Ft+(-ATtV zdb_*38g2=Yq1Cq}Hcy+bTE;Xz`wW7zztS49Q~OWlAQ32C#%q{^N>s|)H_nW5N*nWj z(%gPaZn_lpG?%%t9_L+%QmS;-)xEOCVZ?D%IcF6NHiO54X1{Q4zxCUa1y8dpb43k7 z<8ZuEbJ+~swu=Vy~WM^3Hej5<@jV_B9`)0p4X#99X@ z)*r+)JMWWlfp|Zk9MLvOV21L-fi(PrGOX{Hl0FX(9uL3fTlzFGwnsU#QfxV9s5o2h z$no_n-5VHH$pXF{!P-3I9!7;hsUp9#D|QxLFyBE#8b5zInNfR=+sX@kFg)FfoSe%C z99Ej=>^AL`Dnr*)lX(gOB|-EBy#v5T3CavbOE=KSgZSlK*rI`C+KT;ZoR)wC6nY zgIYvjybrB;EqQ3hJ{TF)U>jfCmTZo!UQi667$v)?2Yn2p-g~gRQJRJD3YnpvUT!it+=oae zGjFcoZ&Mh}(;#AR!@d1CX=(mOw{sb z)UAzYm>k)hc9o*R#SEPrLx`(Zkzm~0<}?5~yq_J3avH-|q-@HA~AKm2%o5~-J9l@_$VZdlv$XuF_XbWKb`)>wf9R*%!-r?2#u z0w`9l1^IGdXLveE_&Xa$BiZ0DLw2|TZ&JQ*Eekkb;x39&$@IvM3h=^;0IlnxBa;q4 z=nymCZ8l*2G%15BB8m8An&ObGRXuv`3t=kBwEqlZR4Ulte>yJrx(UZV`6vg0p{HDy zqK08;;7G}C6V!W7`iuKDdVdW807?)MwuN25(a!nGb8Vk?bigqy?VEu^y&_MVg?LUe zTq4w{fv~@e5cAeTg5`_4!QUAJCAX?e-1>INmoJbT1|pX5u8{5Xn}Ua(2^bJd#**et zRT(_ETI1L&En*VLOBm%;w%v62F=F{ckQ3;0@5Co)WLP=av3u8UFwNCapd6CZ)aow7TXAsgAg&~Nf8O-`+VG7dvd>8W67A#V>`!xRNRG> z5y+=Y{%XcC`k|jBOj#a`;_%3^sR%A+F+N9pE1-w9RCIQh|V1?|I+>{;ksLX&=62o?9 zVJBAGK9S1qWq}z{qPnYSr$>i^Vl69|#R_{;*48TjeIdKwTmun&eWNDFXlPJe3c$ls zlM=3(wh6OI+ zu^`tV>S-lc7EbfCsu4K|I5K6&`ie9U+e&$=_EcARmrMifUU`sCvr}BbzZ~A80iY#M z11wC2tBcyD?c*V(9wl&7-q5DWNZwVz7T!1o=A6Txt(())jP(Ev`-r>${s^6SDf|8S zz$~5prLK|UH?xPXTZqlBEqM@Xkf=Pto{Dx6MnpKsRJWlBMGa%pr4Q^)nEnPQtR zLRVSoi@XeP;Au!8?6H}K0GVxv_pw9j5_m|+K(P3f zE7Y<^8)8q%;A359N0x~kF~SegdiKJ5r+xJjtH)t7xDxX@K-mmFdF5klkbVr_jqzb= zO;UNb^ARC>^E?wr+ZRtKe$23^h4f{=TRfFKSsNjF$DZ5O2(F-WETDGVN%p(4qPWP@ zQUCy>P{dX`vr)x4)64di=5Iy<`_{@sJCV2L#WUx(zv{{qx0D@e^ znjk4u_Y6qT_FS4Ncl8Dt=2o2%FWQ^mT$naoHMez@j2hcJKsnQdsCf>AB1qMciKwJu zPl|#un(+_u_d|wb5|J}=yUBGHdmnGQcJte>e>-+^07_KA-%hlyBKLB`hOy0y0JIF* zL0VhM?__=6ipP1x>$fb(8+9D`OJY(RUcP;}^{q3i^mxS*cjDhHy29CQy@AaFtD@y` zn$t$n#1QsD`iA80Vi*?@d`W2Nt0fku%K8fR1#-WENKE|NIk?hx0tY7l$I?~DMfpA5 zWm#b9S{jt@?(WW|Te`bLk)<2y2I&UrR7sJRZlpUE5Cvi1_4|9D|M#=I_c?dw-kEdG zkdrDF#=FkzsB<@6_mR8U^2be|nE0lOmzGF~$Qsz#mKtT$^}tPg7|q!-twyDX0%#7d zy7`{T_jaii6O9@B)g@3d(5W1`hu)rRYZLQ+;`ZWMZjmMzlSJoOHe_OwU4ENsb zcJs726a}?-)KQd5c>1(ZI%B;*q7I3P0l6gy!!$t662%LsSEp4$ShFR`9nFJg+igRf z4PgmKcC(JC_1qO^o=&sDk60HDSeoZid#T|)-IFAddbO5qwdDneO1Ue&!^n}k$2QB= zX!IsST-D;Jk(*y5PoUpQ!1}}Y641b_G)gXi;_Kkg4d?ZiQOHD3U$XOWsTucR8Z0mQ zW7RRS>qr=|DzwTHNmSO0A)~o*>SN-qVJShO@7*dtEQ9~^yFCUW3kX2-)#b~@D{D_d zUnSy&7xng=aiGJf&ei<%b61z9`vuc^r~yk$&C^B2w=+y&4UgS@tkQiaE~cQ;$? z_svRyLahd9F|6ZHUVARR-CEX=+O_TA8?$#&ynh-O_j=>ysm;)mc{?gTuTeDcee4t5 zH)7g`66AZqfOCAE&99t==!hNQTB7BC&>2q~GP2OBRW%0Xz>W@m2FbQMeGPWN35t*% zQ2E35ClPe~IkXpl=-+24&1Ob?*Bi4n`&?mc4|3Z8V6A{I1+L6K^YC~N z7F}gr`J&N&5+M(Eo=clmmOEw^3svBPtwmWorKt7pw4e7~%?{TUvB0>)g0@JD6~|G< z3mo!Ap&)1LL$L5o#K$($mKJ%a=CFKfo~;Q=_`NcRf)oD;Vjis~E)d+E!xZ&ZVwCiT~LPtT|SFTvGxIE z9TdN@sOpeQkTOQd!VHP+TKR)x95Zk*=PyEk70S;N>vhoYL{jS}Z@!UxQ0JXMOfS^` zK1%D9s8Ni;f^Qf{I$EBVc#$@$Icq!Bu@C-yeox_({a0jazB|R{1OU)OV!#AgGNW6D zQG>u!;I4*h zLNoM8b|lP9?aA_wUcwh5cFj0%q&GzhKVH<;#dSPwhHT+p&4?56ZZ2PK@0*x<+1W_@ z<_ZDmVZi79BO|EVk?_-O=XwKaMGsy*1CPKiiiFKS1{xn0d)!$^HM?7ypLeJ zFYyOxn!`y%p0ngm`#dWzW}&lUv?}L?$EIn)QDi z4I<#w=$fOhDMjp4Rue!`NR-I|8*c7GIuR;(omkzTIj_Tl6x>MuTshvsK)^|$fcMT< zpQ)lTyY_RY|8?9v00w~A7JzQN@k%A^LOZ$|6QH_Oo2{Ckb`jWC z(oD572Ik4}kuf~{@#6s^FGG?qT#l+TLwB0VB_x!$$KIVq;_ccE>|*-lp}8X1G8>lY zm=y}E@B2zv2vpzNE^7U#FXw{YNVlVUU&5{LZosc6hFx2d^yRV(?1vza{DqpJFbQ?dhu;!`J3_ZV{?>p`N^j3B@sJunz^YcrSJ z42^efjAotBO{*pi*KGdlHqe7go8kSsE*!j8^n-HGCHnqBSkXM#od4ih>r z=#coU|1_`GmODTCOg~3>FM8_N?VnZ>rncgEkc!Wk=t`RQ2sEqNS$QendXJ5j8(WB( zHr9^7Pq&Lb53d(Je(r+PB3izbW}F^-3ZMG0X%-cnLnPQ`{{?#0KMnvL_S?RXyc-x} z>_XTxObi^Wy+CWoTK_iU(_DnGP0X9CVk3L}trvw49aPpXBEYYHDhn#n_CvF*D35jx zJyq2|x%{zF@JsxyrMUgr;)m{6XM->OCI1o)lbe0tdMoNw*Jhwo70s#8`kIK;VUp~_x*$*K((u2E-t!BwvnHbi5j42(Db|!SZRb7GmdHB?%;i~S^0Xr z>eEC)JiB6!hH1*FO?Y!U1-?Q2$#LxyCi03>A2+spD~$&S{hLPHt80Ir2ksxP^pA3j zsQJrSHgu*K7JJDGdp1It=B%O#9N9@tJvSQ))2;XFOE(_%;8%SY@W%UJjOZi*+b=AO zgBdD{|Gvypt|N7ruTz{4a*>_5D1WU-`cOTfx1`Oj+Kt9ymCp6vNs!)$k=Lnn8Rb>> zWU_iff=n<#E}oe+8;h8_8k>5fNs3)RE#15cfT!1ol=|Mh{nFp9%G6GbCHPcLxm!<+ zKe_E0p6go6hr>MjuuejXN$P{o1e}n_Bd8?|nil=F z!4fG{ZDGDEL_Ol=npN9iAe2zkoR)1NCOD>DvGfme=P+ap@#)+>{Xx;_yj(1lfw+&$ ziu*}CGaqA^A|a$S;=b(T!!Ko4%#U!z$T}vCfWub%t{XS+f=8GsK8>4~m^RJ?_XDNV z)q4q>X1Ul&35h#DQ;+dkXW(12rPg}(ISDduReX}GCE00ul-n04Jwi&Tdk`o@U;jI*u;p zXY6=1{Eus;qNW`wkOw6u{B6+7b@C$=R9*dP&KY$y{z@NPuuMFX(kO=jK7RIuJ&P6Q zGVhd=Ev89+FpAiQAqr7k19&tyw($O4D17t1KVqgRf)tD`TPFY|i%@`yN9U1{%H?6` zElMgS`5>a412`ptG;0ant4mEapY^4NVmq>(Y1Cw^!r;gw{PO2vwLZ3CGiA==rbV+( z+8@9=2I+p9U@TH6But%XZ4|Og7W*~ieaMSy6@qWRjrBou=57X=jVGI!o1wUW>kbwq zc-wV?1=*vJWG81RM>=4>9&VygqA1s8c=DjX^Fug?F^;#bCB$ecH-1^UmZeVbZW27m~3Q;?mD zZ9;hwr`FrvtL3UiIT!C8WR`&zn~9HfAMKIJY5Qy_8;xBw&K3ZfyRSJugm>PN|L(4z zUPQ5TYP`aW`T|XEjNzv??jA|r&dT~qo6rV6l{n=$Pl+q?c8@r|dI#_1X7EbbeVZuxj-6i58)IQ$ zHXEnhu|fbvgdG$t-}`)2kuUC;z{H1pBPCBPfAYVA=6)Q|QDB~XNc|1RNFFk&U9jrt zvT_-Ak?et&3n@BxNIEmwHjuTtPsajDw_WG$wA?4+2 z2qP>PTsENOTRoGKn?FK5eouTZu0NU-RT4U1*BCu*IZM6GLTF5xbmT+>tb5@jq#~_m&Gm@8fmA4EOX#3ADw%bmF_E-T)X2+FKDJerSk4$XY5N^Rgy4D z8q#)u*)fCirG?c(R+h$m4ku?7_shDT6J7B=^Xia(1*2Lr85oer(i2&8N`05i5qj04 zn#R&gHTf}qF5PCOt<7%e?eCU+=!KN?&^BrU79$G&v8GonwLXt*^*oV5TQyKO4T+W- z#`w$rR5MquseG*<_na9?o@Ulv-iMD*>|QkU-N z;R;n7EVY@_JST`%k^2^AL8YvFMw(d?UfRux`>i5!FaVMm(lM`{V)NGk{d@xhvcd#gzw1A?o#bm>RyG=_i8rAWUIB%hFOqvh;jJj6 zc+&MV>M&kCIa>|>XpHeH%D1Y)j;hSo$h8c{)N}$>c*6FZwxo>{w#XwYR?wn96;OlpX_*IYhRe)fbr%Dq`y>a0Tgy;Eqrg zB8zMI?JO9h6dLy>M=9G{dHnj52gQnG4b^D7I5b_%O?1_@^4Blg)PwH8swTZY&a7N` zUr9Y-rWRaBF@q#{j{+~nG;8*%Dw=*(4_}V}E$qU1qE{#5ay<01qxQ2uuA)8zqDj(Q z&=_iDnEJ<00dL}??6NYKC;cyB0A?rBHNEU%g%%e@Jgv#p3njV`Y<&R^{oRns=0@Di zi~0}sfv!PIr}216TG6*E4aAOhSYz0jA2dq7YbH&y|l zv@+Ptx7rFh5GiTCf{Jl95X-K6PAt*Y;aQhir68)E6JHi9?S;kj+|?fm#tx!gdwXtD z%h?^V8AR;e%`X_atxYIX|1GxkVl6r;{v6>~#kH8v$t-BaD+FMuT1Y&?wcPK2P1UeI zab5YV$9dACCG_^br$CvEbmHuh@I_)I!5nSAR_N7dSoM!Qg9n=g36zdfK|g=hOrsKf zyeR#m);M()q&O3?@ET`UkFN`V8NZ7u>d3Q-Do~b&!r`|O>iGJ`@?jA;;IR%r8~zc- zEbRwXaW*_vyM!PYk}X;e1arwmn3`uWLZi-3UjyS`dN_y#vWm|&4T=5bj?Qz!qU}z6 zvn=e86o8IHer(2FHAp*|M$wi3;jg9&D6Ev2I7%;_~#s^#SID?5y%BWI*guK~^_!_0Qmd zOTz9Q5^ea547J>JV3{)VB0Xey0s?ZHbl!8w&1v0>{DYbLACmw&hT=;tTZ z#NOQA@^yS&Y>mDfDipK;Kzq9O-aiwWNM1SVNBM7j3!J$AWc-A>p4%#oZMnwoHr1$; z_MJA}xhk=RA>Z1DklN~m#|8x?LO&p7Uz^W2%0dG|#m(%yxc0EwWc}L}<6;GlWED zb1p6Z+NexBI~@JaTCE7B#fIH(d=EX9zhEDEI3A^d|B?W4e$9zb3c8N^mgmMh4MtFK z$4nUU6}qr5TYk?wE*+nZ^pDoBASUHVa^aX4Boj#x{e#>gY5LUZzJGsNCW&BP}4e-Lyyu>bqQM#lsChP0qGCwL9Ng%Lp($v^z)0*%=L8oLv zxEo-t+%xS0{Y;ZE+A__E70yJhXJ2l@2(p!z62HG{PL4*4vb82f%>b z=1>|t^?I^q=JLFPlG@UVX5mXPR>}`e2w}7}^I{n>24=*fl-BK-Qb`+x?GH)#R7uv# z!QD;st|Cl$Z22jB%_{onI7ZZ>q3WAmFD_f7Ov#@Q%mBu1bQ8%2`)Oi&9@Hkwz>TPz zDOuY}NXVJDi1+uBpYRh+Jw0zb`Ixzr)Ng1sPobFs6_MCt@nUP$0F)?|CU$1w9Pi>e zzS#yA%h?QD|5ML6J2HP=EOW)mqji*6dX{N!tDgb?9fHpR$aw17-E<0y;%MVFShS0A zp7YAXN%+@Yc~!%f@wEmo6unaC2Pl&BqIl|Z_2>Y|DqBj8&U_?tZ0F;764t{(u?pmp zwfr)}{KXWfsCYU4;$M+zq6mvbe%N{)nS-2A)Rh!TFD7HU`Nu!f+b-Dxi;W!K@(NFd zRG_LbGN%BDzi;z8j*Ym!AHyhS;DC@iofA=O=Wrt1LfUqE#d!X7K8be=_}{nCaE}D) zu>44zhr8QueB7KMa;W~prCOPoX2H;1!+of!XGLavv>M~}PTA6QVHd9;MeDvHO z&Bmf<|3FiU!JNg@)h`iC`vb^PwISEN6L&A1101C#EFa~0H zMd1);Q0O#jcHS4Oo)Wcmt$qLS$Gx>=~!=O zzK_$kIM~jEl9*S%gkeSOk3c7tar__eIIXmXe$bGk1k&`Ru#06f^!RtcXT|X7c_%9? z{>b^ZsCZZtbeWI6@||NNsvqz4?|c5){hs&G(7UE6^k@C4@*^DnU5;?`$`4+B7?Un& ze&8i7?AH3eyJ-ovv@ zmq}l-9v}Fve^}Azmxp#0YQ8|y)auu6Jv*#}y2bt-`3Jc@0BBNN9ml~EVEHweWJno+ z&i!O=W(DA$%&WnTh5`&texeLlAj`_h9yn0gFZnnl>7StTOu3GzA8~7)CmqY@>bJUX z)4|cO5-dCdQ}xQA42=7zneKRR8dReW2Z-$KZdp?a(~#UlHb{xc>7uXL0o+J0V`08{ zOwkf#8bymF?$e}Kcf<*Z!G$%{STHUgj%uUFvn5eS|J=QyI^B|Zd2JP8vi2W)Eo4MK zHEHLrVY7G|UyLgK~j89XD#hlLdb| zmLJ}0R^hsH;Wo(QUuUVmNpow{Y-1i|sZ0zcu}#+3t=45UJ)}D`@f80Bm;8!U_dO;? zo+dj2R=2WeJ&sgG4fyLH7wypC*7xSHCsNVc7+cdGVR-8)-vS#%6Ig~?QL#&n^zhNs z)}jS2s@g{SK`+RwMkk47G9Gd5#Byv_Whb=(_6ZkSZJe zN9U4m^;o~Z^_ITa7wHjH+xPBUcgTAEBWA*EcfX_KYwn9z{54Q{PHga<=)FrL&&KwL zD35uMC5=v$Fq0z|M1c;KGSJ!iPN@Z0EkG4qEB~gsR41J8p2HDdF9unixHt~==T~7;3E~M2^Y4!_d>BO2B=cwB6@TCh)XM0}iIu@P{AGy~( zAC@PW+LOYf;%Hv^);`svGxWHd`@=B20lIE=9m5M$n%Lp-Ly0)=?LOY)S2|JG^M;N{ z;7{ga3`ewbDz)5H8~7M-6WJ3*Kf_cs#*sYr;^~9KYX3{D1AxIM0sUhY>5uFqwREI( zwHV+n%^_^Rz%KNvNIv@Q`HJ}korLW8uDL|w*JVFxZxXKG{atzfId?_kO=$0(+db6Q z!SyWHPAY7z`T6AgRLwljqt53_njk#GbpI(i%3rIILFpOg^;APhic0-A|V0Hb6&Y0uih zt2k2Y3{C!Y!ynsn^LKaYLSG*q9LZEuEXmNXEi2qVrSA5IA_~+NZj$ zORnRKUqkfhU&H@!JuWL+H_En}^fdwQ>YT}7ydpPwdym`D{x|6Lq0eR(JchVG5)+ck zy5Mskd%~dM72745DoUU0eYjfQ_0)V6C-^HB!}4c@nPHdX{a59FgQ(d>c^Ci`=hDHJ zzZlH?2HkguUv6dW99^>C6w)^1{bPG!=qDMC0`(vY3E%~66gvVFxof~yuhx1X3LHiv zR#qX*h6^Rt^j~+g1S@K04x#vnqTD^ z06fwkn!o=7@YN|?T~J}jlMLQ(x=bk1)J~1>bk0v$URn6jSA;%ERZf^d*gi?nAwP3@F|2y1E&R@T7UGqqj?ARJ( z1;Afruv-6sed^$dN8jE-R?1q`$#^DGo(Mtpfd5|ZM{sBeY+iG)q*!6*HhZ4259YVf zys0TKgUX1<%Hd*S9g1*R+#)23EqNEZ>asRpB4xa52ok!QOUEf28sX37^|8=EPz{r} zOiHneFg1V~Kq`+NHBNG$bJhEI2}o~CdmP%hbvs~HYNd3H_z8dd$RlV}`(#tX0L1GN z@1mD58sBN}_cwlLnBJZ1UTjx69e%d_Z67FaNev3~+LJz?33r>zGg(P@5e{~fBhS+mx^VcfBPPfM4OzaZce5cw znTR)%KnfB2PStz(9Y64y0;jeG0Kx;hVt+|32PKI~b@M~Z^*0u!9=YHrDZ`OIxCn;Q zvqGoe;%=04KL`LMu!;XQ+Cn!beG=06oK?5B4&~F3Y<0oXh6pUeT231^6ec%4w$%FI zYmI(34XFyfIU6^N`=7^yq!9lW{(E-s>hnLy6#{_1+GewdL?G5u7j<@5;kesA=VNh) znM(Z2TWWNS`;u?s9JT!*JOH{NHyZVb%<5f8VzIT7n8+`!6GQmJY#pc`vYpHZz){$O z-yTZ?kQ5}MS-QH(oV}J(MT6IcGXri_%*Z~y3N}V64}cVf!G;&5bBmTCL~WcgI_#xr zK-7pb6g=SZgCQrmAo53LOZa}S0tAM53t(y!_AaC5*gwI`mume%)OFVQ4qO~sZ@BM6 zRi0w{;P5eqk~#O6LA5iHP~yDacZmvP_S0SZ!e!$@U%y=HeO7uES&k)q??9KD0;gRy z>xJJCC?#eGB>0(1=t?Bt@Woi~0!l3Qzoe+6_AJ$HeNyH8ii*ux45o%5sl+4kE%-2{ zkCAIq(Q_Vq7j9V^DaSG!6-5lci_;W?|ASm33}~you6;s8%v|2v00Mf(fmJ+|a$}%v zaTjgcIV}&R^{;u}An5@VSi16cnha*+yLqWsA{&CU$0{Wu!*Jf6nnmC$WzO1zC8!bW5#p^=b50BUR}7M)RWE8dosU3T+3 z$S#nr#~g%9b&K|r27Z0>!y)fEMBVz+`>%_D;B8P^z`MEWm&-1}C8me1Dn8m;YUx3A zt65f-EO80cRz(Z0)mx_(j&c!3r3$^5&a-CIDi)jdbfl|Op$8_y)^D%UE&J!Aw_!-s z0Qv+^jK5dU0X2Pdo!p{A7Z>o`({8QMVam!y$Kck`15>sLm*o=xGQyEXR_Dhb!%2hg zh$VIj(W1&9eMKV;PsAV5b~WigMZIYJw{Sx+pgr%V_91~UYyay)5G^2%0nJaLSZSsM z+}K8)b1Gq+h+yNBhK-UQAK%1U1hbBlWi(oGiK7T}1nACqI&Z>1^1lI_QB!T_(t^Tu zfY(vSHyB+?ynok3FlDiYHKsy;hzy8gBUq~l6`TQj=o4RcJ5r4_f$Hmb&4sBbUq+Z_ zeMZkvr%mfV^*(Hh6Uck(~f=)Idiu5`tlf;VvFe-RgOx85!ML1}v@N8<+QkAnFvo(MbVw>VIzcOw;XdOD2 zFE#q044aZ3hsbHN$NeTvl!jwkl^{c47wN$GeCiQ7P8c1n0@aEvREy`p)K~7vYHwEXZr4V{?Z?W9&*R%2PkKCM{PyIXH69hXs)`~SEuRV z@_dWYXmM?=kBfiTp4F{N@v2MXqIy8)${>Ic5ijb#AZtwT%9oF`m3Zf0q+Ne(B2z5G ziG=9_1K{vuJx5OVfI6O~1=bMiYXDNfyYwbn$5o>93bz0PpO7CLTSeP2aX~c`-nO900_iNbIxA$k{uV%$|7Y(PPIr(^+ftXJdu; z6rG+kI*Rv)*_Wx}(7_jwokztT{+5RWNOAzcialcK=|crH@eSs3Y(+XDlY%nSmu}pG z1Pb+$PE!4k+{z2{nMth}gqP_bX}NaqS?%?M zKOod%>MGN|Gyvp-Ez7$>P1R!n3b|u=7^_F53U7tu1^(C3RQ|m*YQiIeVM*P$u9qHt zud4}H#F`=d0JI)LQAmB^(hfH+4f{;E&^;a8t}tnz?w-QB8;u zKlve=_Qm&SVO&l@;R@L>@9%Hh9VrPnDv9dbjYv5iN|6Qb|OuAeZQ=jTw^OoIj`CoSdE*4 zWsF)A{*bh)JwSZ=#xy8BO@12QFBIk`J?1L<(<$F7+3GQu7DJ`ob#gb!;CvFV+V0_s z4wNGXR!0wAYm+Eg2{sJd%IUUY9tNLIPy;~bD~EbDuDXzT{Cyj#*S=hE)09LOCA&p9 zJX<*MH$4iQrHG)$p2DTbQ=Giuw^`sKwLHM=V>Iq0ZqHuz)@)q$&Tjs4yX)xE<|+KB zNUG7LAVjAp)69)~Upuuo;~(VC0JNQ2n@-5=`$9~QsvyPs_*5~!(nN)ool3V`e4O5S z;c%na#c)y+Hf?)5d;N=`hnC>bhm1Qy-0*qZF_Yz=7y_RY^Rr|K+yD%ZLo_Xw&D`%Us+T$-MNlj9|4J!vW#}m5eMOLocW+3FI&P{*! z{I`sDN}M-$W|jDM+IiiKC(8~ScHMtnQDufMIhY5SNN7f4+xd(>^oI6n!Qs8$-5H(3MkO;ozhu{J?6a{tmIVs~* zpb<1jTzPd;Dlm6wY!Jm?wlrLBjL2gyykhjmzhhI44@q{OI$RoQA~qZXV{wfv%)L(@ z&d`%Tb(dwmypS(th0~sni}=y{FMSW-T}g3p@VMT{q;zH}JV>d}hkvh*5s^~7e?%?m zPc7hf_56HsMcPYB+-nKYNCWlVkFolqqw-{$7EG%`5)W+_{Q9n3e0kO?E!7louHGU} zoIGexnU|ul0;5666yD-blXtN7q|$iA7^N8>YO+XnYmL}A^{y+HMTTqRxq9P}X%)=# z$8Kk$*&E)v*4&VNny+5}Aa{fWl!ux*4zb?KpeH1OfGKerTpkf1ZKHGGB|lOmxuM7; zgL6_JoF75AJg~(X4&N1MmBgX$N)6M-Niz==-E*Xm1qg%POBmH8M2sbtCk%M)jOd7R z-FX-+;C&Y&BR={L^z`x1qcwI~9`NTMiO$T!bj9QZIQ+-~C6*&=@)~oMR8)d}mkXU$aCg@* z==#)Cdvu6rA&c4ET}+AaN z{%c8*xBBEprUEPXEaJxMi6{jFvo+QnD@hK>N584v*4AwV6Nf-F&sj8-UAjiCw=Yjk56?5D5g(d^NpGZ*c^l9$fBx?pw|D zpO~z3m$tQS&!G;JA-wb+9AOJK5UweDAeDX^UONcxaX5=tadGOJMST@QB&WTM>e0>- z&w1@*&-?{gcFKcOq{p3jx6cUKm7W(hXIM17A=6H|0!1sRJ;JkYJIg|!P`bD>J(M8& zhZF2vXxE5K^Wq7@e7ql(f*w6!e#M4u#xtx2tFN7W_0s)x>rP%yqQ`-@>dqjf4G#ag zF@W%s1inILEk++u=4f*cjtU%UqtGsnrv@Grri&%n9Br53xbQY^%+m9Ik}KF_6OPC? z)R=t#_YpnSw|h=M=s(B-fLZFMjy4c~A(`GnEFdiIPQt4svMc2vx1kLWV{l?eQ=DIq zTSlnM<*4h6g@-{aYx$>Q{%xxU6{IS8<#n4Z=>Zl1R>$^4Gt26?zb~Z@*(cW3xt7nW z+_A;~CP|LkPkb7ar;<`A3=iYo4}+gY!H#6CT+8Z8E@npHDSJ-^S;hMtJw(QpbQFgM7gH?Jg?G} z1STN7RvQahM^C-j28%!=U%_npcKYc85L?X}l;gtW0uYs7V ztrb44hjX~Utbd7Exqr=RV6v*2qZKi+xWhye7SJ{>XUwh)?2&Rq=El#R^w2X%>7Bx0 zxn}S^fvWdl>1d7;M`AybsG_pY8wLjSAQ2`-y1+lcw+Aip7%;2?G-}~Wx}xZ5W zuu+QaJI43fbj1;9Q87vk{A)uy19nr5!d~3o;OkQ%-t?yyL5hlR@T(Kl5UZR+6`;GL zp@#n2oNXF_Gh!@XoJN(Ll?xXUW4OI!J~=nk(opp@HqhC_M~CGp>8SK7>C$L&ble9h z!6?XlS;&w=iqKnU;sduN_z%Tif^i0_(Zi5=X~pS7Xa;~Y(hlM(IwY`8XYpZX(qh~W zkJtOa$DQn6>wl1=eXG8TSU5&$MpF(jU2?2?uZ>J>Fm#p4wG9tb4len)kM6XJllQ_g z2G?l!{ZcB%>~Y`c2Z07SeEfsH!wV?))q^EtgX*wdWGt#Sv`R?+=seBj%_%hWFa1XD z=1z9+YN7X}qnK)L>NFV2B&(Idi2-Ou_*y}P zZvnX_fsuHgQT^QfJ++(KmO<@x0{lCz>|buxs}e^ov3Vt2N9jaLm4hR{*L3dlWmJd| za+yJk_XO7T=_a!!J|6gz9qSO&|K<&I5^M-SE0ckobBSA<5d}vO6nvzcd0?K-7K=8OIbzLzOjqMG1y#ZFz(&MoMGE;xc ztG>Ccmv{IFxeFMuLVfkv>P4isbI&FXEi86Gz+5I#du5Nrm7hDl)VF*mD!nM%_tM<= z@>qe=LcWRsLw`J8(A(st#ns5UlC z_(Q-GMcRre!2cW{kFFU@5iUpDnsT-dhbLF_mLQ~KTC-9sTkr=qvf0f}9k|;f)n*Ra za+wp#dHz<@uERvd?jI@oCHG(gCnNkQf=a^MxCGe*ufC`P=-(&$@?8qb$#L9??* zfo}AB)&iTZ^R4{v3p3q(iS0y`ddP_6`|Vg{S23+coOEAZPTl?$p;-|A1Nq6d^nG!J ziMl^t4IkR{_48ca_yV03K$V&hFd1+RV$2es_|+8!bZU{y*iPa(eqp-M^)8Afs9j?8qOvY`A$1wYsd2&g8;#-qCc|8{J;f;8MqbwuDugZ72(QqZ5fN>Z3|O;) zdt-fw+S5PCt-ydIYOC7JkV9SuQw=PjbwUk~heAdWUN54$WQi{O!A9b*W`hL(gC1~R zvi5qi0x}l{KqX2YxULQj3!T+wH94^L)!^jBm~NB!L}sA{fBMk*TvQ*?oI#2tEgOSE z>Zt&)^i&3ndw3l$(0y6S+Ny?@jBwbAGcf&K<zOdxMCUTVMf&B^cPgl#~91Co5tWz4o;86@Lt& zaqgy?ny@s_cx;jn+b7B4+|)&kjP*tL`tQ}>42jq;n{WUC6;{CS8PYje#t(m~s4zkd zv90N5!}(brX3B@)5mUe{KIsjN~vXKTZr+~cR;Z;u3i-N^gv zq|96`{)5~y40xz%I(y7^%Q~2#K}8#mu_D9%FBd<{G} zCGJzdHuEGli}Ff~8S4m08@-d&DPT%(z6y9C>RN|8Pclo#O%ckH295Z99Pez#_#`MJ zk1&SY7Fmu#(isS8ZUZ37a9}J;Dg$o8@6K0e%z|X-=s}qdW#`VW&+o2l!!bnY;Bb12 z5C9(G#@nL$Xaf>L^`JV{Vy%PuRf@C9o@!U?I$-_FkyLj@!7e-L5+ANQw*0{ervY27 zdsbdV!wVr}1<1B(t?(U)U+*hzleFOac>4p#^Qnin z>0{5`5k{Mh=Rw$Al;y5Ch}_CGYul?J>1m@j{Y#WO4wWd2LUc>-5^WHEjHHJMa;Y>M zhJ^>BSUzQUaZrJd;~8g+gwD_zG*EuW}l_#O5P*xw?i-(B31 z3SRlQa0ejTIzD4vTvlHeU_uCF&pu&Q%+gg{Wo0CugI~M6*1t8{Gb6x~N@@|E6&z*h z&Cm_WPCL|lRNP$qwh}7Q=P2Ua^L&0wCU=G`t)i>_z+F`wCL@D9k(_Q8p-awbmjTKNS3slGe5)Ss=taU}{SX=Fup%`N9XUhTLlgsnf^jzsTCX_8VGbYp zf0HqfvE;CzhWRWU#C-HFOf=z<3H0;U4zWuq?Wt~Up145x#p6rE6LutGPzzQ#`|o9J zlv$@YQTkHL@O6p6794m9xFput`+Y4QqOgO6qK;Yzf{CYu3^B1lmzMcxo$5fFlrLPOoBYZFy7R_A6=Ta8q>f2%}fi8SlDw7vWJHDQk7H7mn@6tRy z<0hkdR&%QL-_q`G9TuL0nGHi3cfOm=iBXa=x&wd+F(!l7@SUMczw9i9bJ1wOC|C?{->s`V3Tpr$XYaa?$%;6P&$6V zG!Kk;gVa_x_BpOScqm&em<)zHDxeeeweO!Gb6(FQ^vj(@qA__Drif2HX^_)kmOz=V zAAjbgBT1+b7m%E@oYh%contj|U#blnwoi7Il7yZ)eZk3TAp%%9Vn* zV;Lv;wv+c#lw>9jMx8&#B62oR#b-w+UK{vwy=UXzdpvr2B%;b#$^MKrk+{L3CYQ4O z$uhlcrh}u$Ryz!WA`4G>uFie=+ye?f8npI{7A6d1_2W|SZgW1RH!#yO<0xX*){Dy1 zUaaXLmK~|__;Pg=o$i2;kA{ZU1g11Jn!q2M6z@y&lcuzJ>=&>i=N#@5VxR(?nSE2I zKUkIN^NVr50}0-qx`k+TGT@k&a0OV3)#dy8DZrm637cxxLc(M7cu1Z{@i17nMj&Jw z${;5b6UR6lemH*TQPgfT`rs@&t^@z0u(Pxz9V_%%-!4915Du^2+ZZa?qjvoda;U)9 z8b68eN;9mA+(cVVz$Dg`e-snR7J znMI_#s=aaRJkOT*%#fZJ`i>H#d9kCkx}kFXP3Y06c%Jke%H5iiilnX$<7OEj1~-*? z`I5{9$!C#M-uL|qDOJ_fEb{z4BRBCPi;wGI@`|N!mWW^k1993={?so|y-0#aNI+8g z-oB-Ldu(iA(dpGo<-w-D^f6E@K2B(sE3)v&EPHI|R=dAtDFedw4~sj65BFJ>PRZb; z{pU`0;Sh!iN{4*I5Vue+bUuFeKMf zv1N;VbjDJwjOU(zbuNLDvYf?s>x;eyWslLn5Gp$*<1W%t(GPumGIR3t68bn!N-0@xQn%klCkD@6 z|V>U}65-hfj;xUr8fm=%+A2*wSxziWPb;IjuI~ z%hb#OP$qy-rxF|MPI0`YL6(ON#QI);Esbj6tqsfqIY|1$3GF#MaNGrDq_S6 zR*!f?6Di{(yGYfHWhQZ*#s0JY+$Sj(7E!gkCUM;d|Auv!Hi)vMAy+s5mI+G@q49kL zUNn|paRL$Jn=rdH%988X+jW1asG2P5bpGlo6C+ZUm%3I#pzZbazTONJ=Z+A*rO& zp$Ps-0b%cazwOiZ{LZfXoafx_b%eca{MP%wJ$aojt6FSy0}B&hSpiBj(jao)aR;AzhiUmZENb^*Ga(enP4ew zV#w7wjIDvdFNw<(TcTL$t33PsdBxjmltBEtjq;1wSbrg&=cAlqYrotmyk{jBu?(t^ z02J*_n2!lwwvY{3qm^u^{^$d>kh(Op`}y}_;m^HoIlmpFBnLjq$xG|uGb&STHWLAr z)5CsRru&tk!90OaB8NeD@#jI$y@AxC`d-|uS?nR5?}LFZG+98QXyQ}*g0f&12BsbE zK*wK)vrCp5;-Bed-;)nszr1aMc_$rI@B#EbPF z$A{rihjgAC+TwVb8$Fv~sQWr9#I^YC?&q7&SltCfbA_v&HZNLZXp;-S5j9bMowIXF zKox|HO@^*AgyjEm418OP#a-m%#hjG0k!~KsZH_wOqguTL0~W<*6V|U~=xioTKdAeT ze-XsV%=CBjQGuL*c}Z-y$3qKFmgOG$vOxI8f$TYJ8cVUWPjymkNtJ=F=Zb|H*>%@PXXmn)0{Q+Q zJZ@fU92cVYsfRswOmCNxvCve208q47BmR1=#JtfD{9XGxJk5;g_ti^~6OVjfr;ZHD zt}k6D1*MYPC5YyzJiJ^+3B5nYMAp!TL$i~Ezk5O9!>(I;aHza5?x^PutC0C&>hXV` zjFqIb+29pgCtj!56T+Ufy#~#RmyIx6RUwwKlL0!FHX(b|UX^5e_YZ*75=3|DDnGdJ zf5?F__Jj;`Feu64EVmO7EbCa7X`QW)Af35)KU=M%ODoHnXeQt4&HcADhfVu+IE$5T zS6Jvr?c11BnCvQ^+W6?qy#1m+rp_~Y;T(@=BC4040|yf+G}3Gd0@Z zd@TLq?`uOADzc&5-mx-Q-**s`HW-VGFTf?KXWnxCl_D{Jpx->3*W~6 z%W`u7NL;fBxUw)35}jo^j2WipNl@!rcc#EwW4I2DWFb9e^Km|2pL9Dq%drgH<+zJ z)>I}U&{znbEfEh|#3SblNf_5)RVe-n|z zS?l66elAI`)~Qs^f}`GuuG&mGSVl7Vpc7pTk3!-P`iV-v$Ce!S-r z&@DOg@6v2e=mkV9qTM3fHi(hN0Z!Mmha@a2wKD$4Ds}{j}`}IIVcb=Jn+dEAVl4fls!Vg~Y z0g%u^5pcHXb>jE)xO|O0Ts~tT@|dw7?rI(%q|1G)$E{xATN$t+tA7@Aki#2W3o_8n zA1;PA;?(dxexDEY!$Q~xt$NR}8vBT%*17e~tV`0ARd2Gxlwg*Z^MvomdPej!(L@q& zyKew2NM5&dpUYQ>O;U}TnFVimLEf>kcD9@Ba91C1EODuAsy=3IeO>8UcMZ(g*~nN) z>LSS3YaR|iy}S0FHVqw^A&aq%Z#em~9gSqkJR}*Yz$Ieuq?S#kvbOfju z7L`D;wYa_LXZ1Ky=bD<)qP47A$FL)|sOZ&Yn6;ry3FFr0)@898D~ov%8-vq?ZDV}V zTK2p8&T=U&Z*~|bWG3_McdAgzZ%c%7JVlH zdwtKpKeo+=QpsUb1B^5Q$ldMMY^>)i49A`$n7E$F^D3zFb~6XOtmj{jztF3VC88Zc zKRmi;1e2rwPZaAra_s#!uqr`IJ zALP(un{XL@)kvuav5}e@ux)Ex$EA#N_EkSY4krhH5@;$@k@DLZZ&sSHv*HWVx=G;{ zky8i4B7AEB-)>cd&^UoK&p9*+aaOR$Iji1R&w<97Ig-Q*j{w#(E|fOhht<9($f=2}|imVM%$$W)%j|vt-j(}#(UR_U8ld)6u7BSaW9Off@y!$ zj(x@ZY?R<6TROuHlOW`=vj)I1;zSM(^sV+-QqJ<9MxfrLnnw)ml&@egFAt^rcM@Ct zJyA5(`Sn`oB1})qjWQS1fRbwro0YmwGZy7rd#mq+Y6X@}--Hw9=$-q6n#u#1t?a&8 zZ8LKU=cgprqWkp1kY8Z|g4t z006oNJS}t>UCS0$tNEEAdUhz}%qeCmm!0eTs?YbObeQTqNR$ocuQlFj{HFC*p~LZO zWjUh4rMm}Ia@M*u2U_?r6v3P(XwXbToq1HeYxIgKpUaR=PS4RKeJYx!ZeS~HUi}~B z(7k@7@MoHEnZBnExss4!rx?2hr5!v5=ya}=1%K>vrr8U%?|C>&pghUs#HyGQ=qjI$ zU6Swi+w6=hJq*?vuz9a|TkGPp2_k1>M8VW@W}y;Yw0!#Q*wf=?W|$A2^pq5_EB@KS z1_W%YdsU}9y80CZV$M>sj(nORf{|shhf$B8#~;KUKBZ?n$G(84=$S6@Q|+WicZHME zu5zFgbe_xWr(gPYoNnUWRA(S}MPoZ5TkgzmGK)+iL4%u&Yf^=)NX_UqwilTSnBRk5 zL2{Bp#`1;ghiG5IK0l%sn8UAJNjs%9j!Eb^D@%oB$Wnbb6I)jqYt3dJqqj#2qm%5d zQ#c;Wz!q7U9VbWI-EAx&=K+kVKt*LWoLi$dxwYBiXUerJ^t2W_S;jgllPr#9{sry` zDc>xrO|xS;AQqW(%gEe;SB7>ZNf7wGi-w$aVwGOx!#FC(AZ}hPs;)eZmPfDF;~_ce z7AKXvsnu*?42#_Z=-VaJNc}RJe(Dx8qhC(o6v>Rf2IZS~E77RDhqYyOuFr*cT48g06;Pg!l zHyUCUykr+bPyYN?YGx(6ee6+(VHW4weK+v02=0UIDQ1c9e9baF9SPJ;zLX6&gZTRYzf*18%d4_Uuu3yRR6PwK>UCV z_zKU`u+i9QRpgwrIA!C~ElJzO8)9JPR%zH&h@U*DM>*VT`(5*HvWWvsuTJazy7E$* z2-qh`2sMTo*ofca$ke_PZ!^6CMzCO#_chP+@fh7S& zl$a6P!YPt$X@-Ad(p1WN(tQrw?zJr0K7B{y}aJ z0NE>lab`xm7GSHshA<}*Y!OR3&=BkV%yrtdPWVX6N|&+bKiSs!TBi~@x>S9%Mz~n&vRWGk;QLP;vJfrAd#>3)or@&z8d@ur;xA4 z2FZ_7$ht3q6yqQBGqBEbEhRSwmRBPHP--$j-OO8Vqr~&g<$%h4L0&?o=a=*(*cI|g z%~VyQWyFQx-9;_y%`X|J3~(vmW|cAi2+O5*ekm7B+P6oIqgMX#Yf126CdXRcY+bx9 zjnX%z^^w&@J<2Cz-CExc#y?L?oxcn^qjad78hWDU8xWkbPy7}ixywM-R>E z-JiUwJ`RwY7J#^a$ri66gq4TknUP4}u4pqz&oN}Rn_NuqY+A1Hw*h}@kzGzP%zSF! z&9(4a$FW*AReoP`=Z_=3(2Ak7MJuYU@L}nvs!><%DWehxJ~K@govKt{Jd1hB=;Hs& z15$oOL_3GsZd(d1Q-ZM8sLp;hmqY7++jE_q_>-a$JPag?mQ3$W>7VYkl4Ue7r!%{7KulTbPtmystN=A-HT8X$DszBWnxm(j$zW7Gs_6 z*p_z-GKRn3SIpY2`|qq_S~Q^e)ZVc|pWkn+S>8NV(l;~RCst!SpKknI;RMLbt&z;H z8gVhf3=C3w#NMfhJ3@G(NFz#mSl4}VG;*`I=)b~JPo0`0M3x@o-_=%U5_8gWCqjc- zDpcs^m;34Z@$S_H$&y(eE>s~3z_g22+9DCP9II(-8R1>Dq4`+?bD3z)ub=&W`44jF zSGh@PbX|R+r&09-pV~El-0q)Y)jR&_+)5Se#7+9*GNVdo=OwO}`$Bufl(4>*J`i~I z{mR$XZ(`ZyJF4ztx1F6;B9VHlLnIAfbaYQr0Sq5E(2xJT9ih~E)@)X*%WpF05X4!# zXq(tl(^!|(gT3FHrp>dE@XE2qV&Ahu&kNT(Lncs}j*qovYNU_3Ue+~rNP?X5iof)G z->XX+;jlz*QY=*hDYkO%RT`JV)LDHPeqHag^Ydv%{6;|Y2iW3MR%u`p z(@~2|j*6e4ZohLiOW(x1C^}B9%m~j2+@;)jw&5?IkFp*eUQPM^wCri>T59}14}A-e zHx$v$9(?U99f=m}+n3=liD~q|r4F2@t+L?Ht(xkYm|PL$9NoV&P1vEp`TV|6*)c$& zCYZj!@gy-mi2ls(;#M61(dEWU-;8D?2x6By7FkCyGF@DRZITl9H6y{gGz5i{bQ|L$CpZA;=j7?B&#e)HazaZvP)Yb)j_IX13)}M$tr24IdTbmFe* z<(%GDRGuM!FJ9KpfvL8%JO#e8wG^A6>fs>5kXRsasl$k_Vb8&|Z>PXnKh^0uY4MH8m$b-t*b#h{{(R}@u~()mBZ&I%a5r1y(>?%=W?j%A zTeS$2%4tC`v=7e&i>16YTKahiWU*}uL2-O8A_)+sTYtQ{C>cVfefcyCMHQ6pMrs<| z-X=bXto=unnJ&*t`Yly(f17-mqW~H@=qScK!+p;Gy8tyIi{HDK5}qz1uxrLets+|9 z%0Ei-asUZ{%hp4Qc|v1VBD-h|U+R$gChn{ATRi_TwE{)^@35tZ4e&Q<)UkM-*I=Ba zDivwuil=Il@9^n9UC64Tpm6BVeaCK9DaOpZLisQw?*jE#Y_3>9bY<;jeo`L%m*utr zkfe+Ozm3e45O&T!3#>!^Ee+f;{yh6juJbN`tp76U^;WIG<+q!gzThaL!qrUM*=x+$ zE@?uz>A;D!r^atmRVqnlXKR7Ln+qhSC*b1x2>&y=&JU?*vVHm9YC`sr&g+}|y}2<) zwu#sn{g(C@?IjF3v!Qk$6G1=;KQ2gDmwYCfx%iP{L0A5C6#Q|3K3EMI%Elxv)YE#P z%IEJ!!*d!vHMk-Eu0Y?=-+!`GspQ~VDkfxTP>Ywd{`6-3X#|>@UQkBda?j=c7yI_P zi^5$q9KcXr`GZrKnpce@x5M&;OoV|Z)^VQ@^SQ>%fxGjvD2j*eS8qs+)Vn{*DEhXQ zAW@0sAq*3aqD-Rs95RxH4~uuE%V))(vjfvta|z?elR0Uoon~I`NXoNK+~EuTgWP_! z{Dg=$O}KrZY-Eijgd;H~{;M_%x;%8}Izjj|v#0Qu7wBp0_4&z*kmUP$unM!BcHN%W zIhTY|T`muZ@DK>A z*tE%;%usF1;Eg;A&QHE(@x`f?)Mje%ood0B_BRBZ$qh-kV(L$KeIHu#s9T*M52#%( zrL&6!WA-2_U+!%m4hdEaYvA zO_!sjcgmk?8M5Bcp7W$VWjqiPYPz{{M4>&i-+}0thKW+$Qez_5o2J}Hh>cP;1W9kx$EDnMwTl`IP>m^dPglg>1Tm`2glVBbo%s4sRh71OsyTl6 z8L$KbXBG@Fi&F|y#!8NdMPBnO10WN1!yOS2d?dIxr!g!r#aTETe;`i!)OP?saV8F9 zQ~a)Hdop5j$`U-D5d{U8xxyaNUVoc=Ib4!S1JPlWeZlMa-`$Wk$YGs+r+`Q{Rdv^E zckGux()j(sLjGFW%MP)b6^cxg&p8;`<$f&k-j8KqVXXcoF!Hi!g7;n;GpiNyXtQyty-twO(KR-su$74)NX;wstJYQR2 z=TKt52`XqIp}X^Tq!dC8mqq{dKBWlx*TsTOCjC+ z?)%>eMXhAyHeyLs9@+d88=GJoO-!nM$>qFS-L~!x)}7@ust<_HWd1H`+IjfFwsmas zyP9HYiq;1$Q`~TIB`)m;K1UJe(gvE`Pd{6bet`p@?Tk2Hsl#ojXp*@HG>XhtxFG-@ zqdC%0LQBXF!~-uFcs)$-D9=OtUwnDNR|mt8rwypkv|ZDIw2$iy7^%MsC0Z0608)Mb zx8Ym!;lq)5xq)S6ejOi`s6AL9l z0T-mFq0c`^_Q1#viVRV~zb!9)4w^-{!836!FKwua#l;0#2Qf$ckS+7*4nc={% zfVisFiN1cI^U7i`PWutP+XwVQQ{xsz^Rzg8OjD}ZT^_r5%W;yjE~V4Vv_dZums3y4sCKV(qdcd)H2+tGG*hEXGz!mgWL{M z-T-Y6@!aCI;C?PGgrfr@!7RU<;FGz$KrV#TJ19OvEMoVtnl*?hj4s}CQB07CCokg|b;Xm7Gu;lKy zkQkXicrzmOEQyg+-y8XdrNp9Tz=PzggX$JRxFZXka{N^FYd>>P-egv>UK9h_KggjY zr!6wCilJC1qSS?hjvPLkqOL&b^=J9tB}u0BQC|62<^hF$2IOd@K%z59&9yS>j zm4MW0igE;=h@4-~O(i2nBUtpp>hMzeiK=z(eKniCmms?}JGqeXOphtEot_+Qj9e_G zQ~N$NaNP|m2+AmbUr5op`sk6QF^hxObLp!kTX0m&*wi|&B#9h_iYiS63sltD#T27I zaL@#=I!~ac-#&H1lY%F6{oEuSZrXeQy)O+)IN%UFZ~o~;wp>leDwgh%C4>>6b=MxQ zus@2_5MOH61#h-k+f6mFXE%MMi^(XVl1}2BYfYFcDAsjEFMH3O-Sp6S{-ZwZv%1IK zi5$3M7XZrS`AIStkSz=SX!ookIy2c8mDUF{srzJVqgZ{ zu^z#HODa{LL)dEYveDOagkG~9wErOImGRRo{i-}+QIy@2j8#}BYbcS5kynvh@}St` zL^{Kw(G!ckck^i)d2lo>S0XZj0(``o|CjlI>plz8OI?YoA`+|cdNdINi$z(%b+teTmIP0ouRgJkZb$8>D@ujk zN%J4R&R95DG79N_x(TF;f*!STCSQq>Fpiz76cDNES9l(iy1Gu)^vM_xFVk^+Z4Y_q zy%wKO5*TRX9#DKW+!|K{&fwcLn`>SelMt*o3R_xfI^{{&Gb&lVe@O$~BIO(hSb}iN z#&(l(ez^C{z%@y5cAB5p^EMD|^!myt8t__eJ<5EsUy0Wq04f4NNWAYRM$I*Ss;R)( zLKvxxf4WpQV)2onXzKxP=i<03^V(n4u}rkyMm=lQelN!u1;@T!GNd$X?^pDzx?>qi z@75RcZf6$@$Yq(?cWxuGc}%+BIpB1)Cudh70rg}RCCiuLX0B-Xzh>koFi@ec+r%s@ zE4^R60~z2Y0IrP{AP?eS@EoU@A)YlO%G@UasAA)j0cl(wQ+auX)_-N9Kil3I?LW2x zgL0Gk6Fzk?PP7&mg)n=>;f4P&ennBni+KL9vVv(dtdp_J7(RTI!_TEIo=1 z3s~4JdG)$}yd^Q9sKpnVGT~==9_+Z3E8PC}Rbb}FODC&eU#HTSaOazi8F=Kq&tA@d zTeHaNFWI$V2493)w9-xE4ycB+>i0Q$cBM4Gyv^=*yCidqI`_Cbc`>A>{MhmKL;Y;m zr?6kq*)$T1a%H)P;{w!m1;XDF#Gu95!O2|Z)j~%Ndh+f`cDUZleE{`5n8PcT9Ue!G z^C~3k%Ol-kgR(R&yyum%U&Yj2Vz$&R);c$Qj;dmlzAL9}@W)u6VE%*L9#URcWR@n> zUPp|r`U~SAFFyCRE;HKd>nztPpFeiBC@{#`N50fRsIk^~v3#Ljgk!;EANBG^G2r;p z+stgApVq-PW2Y5A73^bxH4Ts!0cFTuz`O6~da z9ZM?_1^NHlHC_1nMGtJfM zwT2UlS9;AnY51(6JRC}$%{b9D?+*}4B&f7Qv^^C|508gA+=AmMdgTXcj{??yMf9pj zvUpgcl6h^t8xzZU@59A0mrV(lfa(wL(!X0#A;CYZ<1P1a#KYe*Xkb5nUB(}Y3xZqY zHDZi~;^3=ASpE_c{Iy{$G`TByOE(bp79YW~Lz9XseA-7wd%xXKR0`k@$#Gc@lP9T` zw%k0gS@mnquiX3~xji;+9g|ruda%v{<(Du^!S$7Yu@W7gZ1AJ=;s5il9RL&|@?{R& zC{f6vCIG_j7Wa2U!?8FmdnVU8pD{1?Li6-!c+pY&L$~_I!iy4z zoV(~nBf${ZBBV)OU%4D>`A2Tmx*fhX=M|$uYeoHhkxiDP{(uSc`01|)v&=Ixn!9fV zJcYCnUml0w z|C&+7%izlRRo4a)GrRVJT2p&1b;)FO`N@I7f|JRWgwk*d$>X)k{ih<;D7`2;CG`|h z5pt|-OIA-tIAikZCgbMpL&zLxMl@3ylg$Jc3^5~ypgIco(!&~1K03aV(zLexIkP1s zvl)OIPsQFc@M+z?`LHQvt(&r>wKzP=eRbj-xs^=<;?RuuWc2d>m*vn)A5ugNJ0rQv z*h^hr!E?ii_5kwgbxP7H}Umh4i%Sl2G`_U4i56l&-=dE!BdD zV@3CuF`nsURn|vkZIaf;s?uEwP$3{0kF%2lGkNItxd`jUN{({hY;#;>BxZyOp;A00 z%-S9JM3zoH>!Ue2>|M+TKDMQ^GqU1jD`(UZy7c{<(nziMs0a zdOv$^5M$$}zciS-bU*~BxrU%2U6G9_q3VsAh&`XEECf`*!~g$v8T&{~kfxE03>=$( zs~f*49Z323)F3DZAW2|%Ats&0lt1;D^d$$sU#{&IV>~HzqM`{7a2UdR5?H$8y;pnc zF7@RDXXaI^D&Jj(7}{s*0rjAlTQC`NZuuSkvHd3sb*=k7)8l6ukUSp(?}PNX(5F(_ zeQL7po_QOb(3y_WbSq7MqzEUs`!unMEA62qx$!_i77E zilo!jo-jUJ=fbePbBXU(Fw47fDv>Wo&0i5eg>K_@x2RC}Xj3J{4~zi(IYcB`?`S!R z)t1@x!R%0KCNgfUu^Fpw^05v!2>bPN2C~&F2?E20Fk-;4v4OIZrzFXu>wF!RfPg0w z_eK8&ovk-Sk5k@47ELhpLeQN7dVD!-p+O+u4RGQkUrn_6sJC4M5}YVC$KU}6>?&^4 zMuz|r@>9K_T!6%m;mLQ>S8|w-I|P{-!IMJGx*5k?oZg&VzH>2q;)y7_M5X}% zk0rgJ4GElmkW_Bf%ei_46yzebq@7qMSP?xLoS^^U>kcWuIW&bDH_cP{Dr}=^ za#TX1vN@f_r{*q(4B?Xgx3bo0Pbx*98Pa^XSEkRw8R7)Wu6I`HZh75GY@S$_zh1q4 za!{2+^!qOr)I54na3x+Xej?L4cAALx$*jH<%gjgSxRCaWN;}kNzHXt1Lzq)8PCm<- zQUXq&4W)<2)wj8U`6e{v06q8{W=A2Gmj06A2ufX}lH(f_(}Mp_DmW`n@ILsUS^cMr zr^myL04v{)o>C0wGAumCNQ=(Ela9hUV!lUxbV~qZhPdxvmeY!mPZKdMlqmvp?Mzy3 zMgws`t;59?655&gOEeY<%4W~!W`Rt^m9D?$ae@H+O2&v8cvr7RaE`&o69#0M8#{hT5x<9OX64>Ac!~4PG z{MK0C%XiAh2~t!ANE;*!mpnaBes}k{J?nJ%KJz2@SM2g>r^ihf>i%>!``yX2yHQB{ z{g=iXt);3By}s+`}SCV(2p0grZBu1sz73%y=W8j_XY?7S@KnVx3nOw%!MD0V3Vku3|>`)+D65Ji^u`zXeCJST^j^ z%?@AVHZt}JB=r!#p@~t5o1Toy7}Us^{g$#`R#4cUV)pyQ+8Nb5qE2mn&b$vzLgN8! z6Xs-%77noU0@^t)bQoU_v-AU?3~c?%Pw#mH5vJ+sCQwg=PiQ~Cbso)FvKmDnfVPzV z3prXJAz}twnr)31O!RqB#UdGnnZ35DPhJs2e3ozG4^FZQ3Lj@%(%grj^lWuJA5oL5 z6(;%3h<#c|qm*+RIa4rIJg~_nD;_v-)?FgUgN7X92@(Y6TsIVegQZHI#8&VF37hwJ zK`}^@s>1irsDFc0Hle~B+8hV6-^I7QZ9u1sG&qxQ=N4cio#i7>e>1(BAm@F12`clh zZv-&>)9`bYY0|LV^iT1;=|^)lewGGB%Z0ahp;&nxD z5w1&dT}nqsu3{bHl4dh=TFmvfzC!UiZ34PHcq0hF@{}@b#^~}OZT~3XpcRN(G<0|_ zJqo=@cmWYB+nQj&RIw%4oe`3VCVZT2o%o8{=108cRNJIh@Ke++Q>sD3&oURSE-ak?$zQbtuwW_#PMY(}$C5tV|wI&nZt6g#T<2ntvWp-(6N=>5AI; zRcRHbv^5*X&T$D4`402a4tILf^&zcCVSVm8Pb7F3iHgG!AB$$0eI0 zzy@N;T4C)Wdjp&gcd8;$qqdVp1{iz*5t9iB)Ih1{oh40nV2Ue+nBr8==_s{%zVA7~ z%yeceXBI2IM(~Nokl;pK;?w+X`>@KGl~RN4LNmZFwzGY5wVJI%y^ zfx}Ocw(lMIIk(ldz|v@$Cik(7M3{AEt(3~YEJqzF?;&fNbIi^1q@-37D_lIYKdywCNqUi11D$(zgo)CHU!p9+O4_FG1)qBoWa>{C;o$Qui;>K>9CaSlImVFb-b|E_md&eoZ& zP2>?KkKWdPNFk$q`p$0e^x-tI5l#|-LX}zMBLUK^|MEo_jGu9>s*lH7$51z9vqh)! zZ;4=IKho|c60l9hO%6@Ycl>@U^=qag01a7>!<0JxXcz?lhI$5&E-|SFf1bUuF zUql|%r%O_b%kAXOqgUOM=}oWDDsES-cF?}y;ANqLI#TPM{eIMkJ~U~iN#?dqd00(s z3UZYIoTzV;VPSiLQ1{c!_{NvZXtmE^$RX;g6eDTuy}v=oLwA`nsxt%MUci!q#i9^> zE=I)JxMtAvPV`p+@tlThs_-gl(5QYX_8B;=F230sVuVG8km2x;l4Ey2w>xZi&Y#9=+*duUz&ET9?RO zMG2{i(#89bbMxzO1u2|MG@cbV_YieXKyvqBB`tIt4MJpD=4urkTqIH>Cu&tQclgo9 z1V9nU!tBYDClPJZ`CYQSUjFZvlIQJPIjSHc{(PvvW!51NmZGR)E+qQ-^}FM@$?Kx$ z(`(hBp^Kh{(eN|Gqb=5NYYeDfUoZ7g{JysW8};`@jN9GJVhy*e6a5-eC1UrpNnBS3 zO>%jjEl-ayt0p_0&|d}MA*t#xhpYwR;3vgi@4AyC{Xd=RlX8eJ6ZD^LUd@L++up*C z9B!y`&8OaCAnB*%hx2$oR!sNNr1MRo8GiG5&wYOBa}RpkYT5Oj)=36ACcay`*7y6_ z41Br6@1k4Cg2t2-w8L=WjUv^*z*#1QSxBzT9g!h82VQYw!8oEjnjDnN8lb6(FV@L$ zYjBz1VpPAFBW;qi>1)oIdL68qb9+$}oQow_wqbB$6zUl&@dW_NFa0H{G#Z4BVMjg; z=j+4AIF5rOt=Vdx7nkUc=N{zO@tV5luW(#gvBh~1*SPb+9ef2?4?=@VCayemxSAa)_uC-mdq=hqY6oo1T!=bILz&Pk zAOOyd(E&NZ2Fl1Y@eN{(B2LKOB}6jpKVN?CI@TW|*_lM9Pw*3rQFRrRv=8qKQ8mO2 zY;cZ)+aLxt;_i^CX!Kqw6oQ;iDDf5@d_3(pP@(GncX2-P-xg^@U=ihW~FQABZb&#PucT_{k7&s5uvS92R54X?svs4{ZVe@+G5AB}WUx zORv6eQmm(Nv@xF~p;u1}9-qRWE+fAq^$psmD-`5I zEv7ZeJFxZk?7)%m$HPjj&+Z6^01OZuultWbVVZq^oq4q|jo@Kc!zULQ>hHkthibn! z4jlH++&_;D_nyJ2K^Q#WM+B=~6cPmZA!q4=@t6~AIhG$rl4!y(4$!V8O?&F%eRfKE z*Fh>A(IUC)4&S@9-8aL@egw7_gq~;qddGPL)_16EwNytG0O|K2hnr0CF#el!Z9R;KW(tw zD}J0>b*WWa!K=nbue;2Ds*_Vc-D{p!_lNoqo#A;n`WF?)`w+>GS#b4W(;9E3 zNpBT3Db0xrs6#{3dQFLSp+}?nTohYotd*zxpbs}nEa@vUCpANVHJg@yJI z07wBUq+(lYKS3xiqdSL|cfgk)3dN9eoRA~g2Ic351BR$}gZkkm0AK;(XW?b&kkZp3 z)&5)G?px@%PgHq-WhB=A88NyTgSzy+xbl?q?m6&DS^2vizyw+P#&4rjH8_&Tl+IF)?F1aMd&66J=Gx!_;FvEXY zZWYt0%+XX)n{#Vs-8$miu_)j;)mV*l#06K^C>30U9v}$?>w=BX-E?t;DMMs3->tVbaY*#&7b{+lJj4P z>yIu1OdNQO$fA}b0b}}7QkuU85C-Dgq$IrsrzkNqDnl~o(#HH4l|P1u-h+k$ z<89FCG-ClTt)}!u$oW$+BIj5wNH^{JBs^{lb-s}dM9_3cKMySR>=z1mM-}(-UOh&c zG8K{3$^)!>==QO=l$Kv{3h3}Qr6F#1oJYU*T~zsi%17_&E!JUYJ66WwMwmjU=mjNI#24v&M0-j7T@r;ZGnczgBJ zOz8EKXcyV4@n`Mm%LsH|!V%e1LFobo%v6vB-F40pRdTA3D0V?jrtU1Qd)&m5BUEqA zIJ0GfW0OU|%lLn9B%?@>y2gf%4TUhb{8|ARGdcQ5$V)E5Kxd5XgoBY;-T-`VQP!4t zYAbfNhKA~m+c3?T4B6Txw^b^fe_mc?8LWB#*e2JE>bi#MJ1t_&%@9xB8TT>j;R=D^ zRj@@^*WS1;IGs;IG7C(yyn%U$H_=E8vm5+GLlX(R1|aWzGjHf`-(0} z=9wGVZ>Byb*)GLKd&`6T}5PcT;IcI-4# zis?{Xtc2EokXr*lJpUrcD=b)D`vMHIjkXtiAs6j8JI{L3%9z;INjgvzU1^1vD=I8> zxlw$g3N+k{Pd?gy_0 zPj1|x0)pZkZfgH5WDyF+kk-}8QpTUm$iBeQ$HC>P%P{Npd&c(@m8nxzsmPj7!}!AQ z1up;^LNx1o+<)U5=*LW6jyy9OYnyt%9ewVkO1pRH=$pwUSsv>Ed4!1}Z$K^_I)b#9 zuRKXX-2%y$J0eb2`FIK>R51l#9uWvRD-wdHUm`ZIxH;`za%2s^fwoAn)kkm5!J!ZqRl7`d0FoFvG+5(p5%GCaIRuMd8U7 z0ua->4Tfe?nEJ+3RA-h@H5MWfi#oUALL3+L77%I9fhdhXX z#t!q5p4xt=FH4hikUuP__ur2We{|V`gD_rWsl?KW$IzI`85Hr$x+0R|ZhtWvNbN8* zJBlKbd(()obW-V&Iwmo=FR16A9wR}j0r8d@F;Kc)r>4b8swbGFhPDt`bfNOE}~ro3oj8n>`Dg9Lv>? z!kr7cp&0*dh*fk*?w#)w5rxm!pr2TaN&t$g#OLbhS>ae=13t*a!APMtF?C%OM=!p9 z%7-H5vH&H#P{uKavDqUIH5ZXkfRU6Jpq<>G$F-ESfjtTtv&_+7@DVI2px(dB3VFEj z;h>`Y@9u92!4-g$n4e3fn9r|=@$r=0pB2>Vv{qEZg`_+5m*^I0aQSreW~O#QeLYjI z@n4qPk5C}gME6~fhD7Y8co><7@TJQg4jMh6^%Ur{N+9pl=bnhrUz`sim3qBkx>%!E zjbVGjqtneY5Qd!n`nqMny!NjymEbNo*>eNVhykah>mxwB)!bI&`+kc<`K;*1GyJNn z_+)e$lt0Yd{p_Z|8_%L`03a>gF|9plaKY{VOhToW(cTbNXwfhzsJZxuSkLA(`RiwC zr`nB9%DnO{W%`uj4HXc6sAaUv<4VYX{^Y&|?%2;Ev)K$A58R>1<=0qx&npA3wdMsa z{-W-&!=Q6tMju8BCg|t=M!Q}$+S#-g0T@tHRycu%VdxN*T>^F3=r@CTD<1$mLo0r` z9y_velGl|z(=AvpG$oDHVZXOC&i2`11^TA2vt!0NafGqFY&)u>-c8hKz)fG7#U7Uz z{tt2oSeTg7COP}`5#09b36KHniS?%E5SBJ*6>WnsBY}dP4wnJ>Lf)UHOh`legxz2) zWtMVxnD^2^sbQyt@NDTsOqwQ12S;*8|2{1aLVp9|XrNodz5D9{){Q#6Q)(3Tqv#_6 z1a-Ut4#B|RyowLFxD_^t=&NP2o3kvvgp_$*fQ3S9Xk|Dq%u*abXzfp>at2k_CPx)z&C!3(sL2W-rV9edx^DLBkk&BB}0ETuX zLGvLZFkPj;K4N9S7PWtpKwhdDw|H)&(NjHJNPc<&C06>B<*>8SKUC?z6ZOz&Z<(kr zcQN3bFf%~#Ka$QeAj)ow!Y@O2cc-MZba!{7bW2Do;S4D)NOyNjcO%^$(jAg22+SS6 zJHO}Go@dV4XUAIdj?!_`Nb~}8(A6T6EEltV=obtl_(?GqJ&imw9W`A5f&fty1b%jh zU>()@o@;Wsu4&-UHa88uk^NwVmwSDK)XpWr5Xsvi7$(p_jVx#Gw*VB^z+do^isM)W z=)?_iTEt9f)<`Sw&@2jANJS`>ebhNVFxPjO4stBeZEdOV$d1jZ`Lo~9ps)Z}mgU2o z;>{zH`5`)P>s;bqDvZKHmhw;Y)t148<35B0vH2s_FiH=%#Pt5vLWGi}HF3aXJDd=BST}_6K3&wXQ-BJ-luJ^Z@yaEibv^Ycjgz zw`PlF&HHkk--2+H--wC8N3Ii|#fvOP(pV!I5CAFb(UrF4+jmv-K!#Vf1JiA!>fb`< zjiPeRUg4D$%gHhgjh#DW1$24gos%>?PFTf=9}UaOK5KS&PH%fBb*=?{6CLQ!H&vAd zL;XK6=a{cZ1gY19iE=duH)0$tqDL%QJ>=6R*6gA4JR)57vyy$^c` zFcI(<qr5y=JM=qMs-8qMD`)Zx?+o<-M19Ki*4pat%Rs(z^rDknjHZQW(I!=&{w0T;|KK?ODm;22C@1>?F zs?3(w3vvgc3Fv%Ft-Eg+xQd({fj){zu8DUFg_qN{1m`FOiH;VxhDWJmmSrwxe`HB1 zOSA;IRnS!8<`2G{9Fs#&_`{Bg`JqoseK{Zx@jm6X2N2-=4(WT}aepLwj7*%kCtv^1 zl6xP6s@A-@u?AE1zOrD$)K5-FSPX6K3PZCVH+#zf(*^uN+B*NpVW2;(uQlMaGlju* z)Ey~l@~u@?;wx2_o(pBAh@ve6dGx%g+dIP(zp^q-T6s_CbH#l#?*^rIeh^x0U)*=- zV2D9pXv|eDOFj~ui#hVG9gdM{|ZdmsAr{#R{%9?Kko zYzNj5i)x#Cq+2Z{)-&{Y`JMVnT+kU#orPegsjlKE#5sGk z2Quub?{0mJ8I;Sn1giw|iR_yYBkr?CMgMuH%8e5@6!|`rh@{*T?fe1tSWng3$tOAj z35WO#eHg{g{N}aL3`Yc|6PrDSh+o;dC#>U(uIk2nwnsv4(Pk`wX7x?`1L|W*S(Vh; zusNS-Vyx_KS41F_k-n{tU9Z(Z$R>1e$O(A!3%>CyMl&^vn-YK>?fLD78!^%e$0M0` zBrp2a@I@2RI?IQsy$@&BMhw&>$vmvozpT3n!8^qPyNm@Gt4nnVA(k{Oh_N}>dA~^f zu8Jz30Hpn`0KeR1GD;<+%=oh-b}5yo%CI4yhon>i*3%Dv#7q*6E0R8AhL6-d5%5CP zkR&^a0-@dWf8c8&Avj)AV#PZ}r+La0o;M4UQ zJ?@kP>tB%D#{wntEt6wV@Ub!a8PHgUM}^Fp+C`9PE?`#LBa!gvhF0A>E}5 zN^Nvz3PRHKs9J*yY*)K#9I3DV87X!j2)~Iry1u!TxR3uRDBPB~T=*mJmvP#wjrYt0&yX>(BM!c808EE<9_a#iJ5p z1)>Q5{zsZx&dSuC$`yz75fDg@)TD7>KjVpet(YK0FWVUNgB!3?LQ#re+awS@h$p@a z%eMY`QSV4H(7$S}X+I+kSYQQDuaFn$GBIZ)KDB6lvq=m(EqING9x@oZjK&rYH?4b1 z3eFaK3EUCZPQnXvZ1V!4BKYim;ZbgP@9bcDG%M-vqw6B+=jO-gBI@1CzN8*$oK%tZ zDgf}6vHE6d>S8~lcm<%AXknRK7wko__9%Y~{%JhQ;=uPrYi$A9=A;{!t&>utFBVpv zSURUI>)s-RqVBfi1jR=#bU4u|#*zZZN`r&6~3yB%tYRNw>c-5O*R5 zCA4@n&Ii11#NB-MD zMJ^0E*C*Rb!D2Fb6>84R@3I29|OGETiYjZ6jB@{c1N}827lo4SPig) z0D#;l@X92>OY$4-4L6r}i!i=q9j%k&Qn!QtM@jD)^ezNMM~OH*VQrfOeW_@-({UwdFPQ6v4Gt(W z3MCB>nzT@$P;n-D&$;Z2gSq7g{skOh3YNeBS=BgPjmv2&rlYgV*NY#hN;{Oz@?GQ& zn%fA;!mXD86bgpkB3gY05ju9Zu>yR>puIVD51aSG&gL=9**q6>i;#CsTkU$5e*<@t zv8o#!NJgyZ^lic*h!(vOlPHX7S_$9e*X0JA5@n~B9{7~T*tTs7#op!-sA-S1ELTIE`0sDb6|B%sOc?p1C8 zg-;-w7f)5l{6`FJqSr_kkCpa<%c%&V6v=B5k`YD3_eHwd3`|Q9rFJC~ZY!79G-r?h zT@MJBQT&Fj2eN$Pa!zPsbiNS-bMM}Bl}#UzRlt0a$Oa6HS84e_B_;%x>Z!F>TXCs1 z-uLk-FSp0TAS3sK;7^nEWMb}DfgU2b%Rf8tGu;&RBg_;frH}ij$s27h=s?fL!-voe zp!~CrnCNlFk7)AWiCtiWC&Y|f+SDq}jhCDJu?vO483bNQMfGGv9!T9hrzXLvdgh89 zM3>A{T>T}juww*d96Zq)Hmu%Kx_t!!0VElLK2PiN zQpk*-oaBHa3}vb>zS*8!2Kt1gx4%lMc)7Yg-kTSft1|2gl<% zRpdK$_<&ZrKbx#Snl`guy{noDc62>0m;*tBvwZ!`RbW>ESS4~k$iMFi# zCdiH?4FfC|`jkz}?QC!JjlF(cII!Aba}-XsK`%rqc&skgA1czFri9SG6~;6*W_0_d zISf}4OSLx`DZSk{u)zreB;m;+a5ZtHLIh)ldJ=+HSu-YgjYdXoT4QFvXKhS}+D?xP z1CfvCU^R9lYK1kBzXHN^T-F#~XXKN&TSqEv?}%c`D`%0Mn}{ehFCpIp`Q3k~+zCtx zl5a_9mw!OGIQ@aa#ybjUN%uW1_Y9C;VPBUnOkZ*DD?2n-eM7A~c|^m3UNpM_#l7B* zK}9CY&b>yo8uhiEfce7%z50p(vX5Ylu>O$Q&(FHaLnaB*4b#uKWCY$`-+97W3DI@C zpSL#vfv+wxnFi)4zuUPRcQWz zr#3MDIq!`*03m%1V2Ud<%WXK8vNA9+ut#@wi=4)bT$X7nqrLrI3JNP%40u+e1nq*< zM=P%|)?SaYX`0#K<`V7q=sp;E+L{q{*<<8N$Fa)K4G+QDVdV+f`xUlEkukj>w;K#v z;9C&EHnwJy&rriaOo%vI&_gc5nl4YT@}!A)?lAPj^qy$l^N?Z|CQn&xrzw$zi~y#6xOby|Wu)_8)o2M6+C&IYEp1p9MHQq2gN}3GBKQ(w zuQ8xPJpVB&4Win5r_e{In1sn5`)tb!wXdhWt0H=L{*%7=up>N!m;yeiLu_2{W-82;MhnTkChcspu=v79GvZ^PV%9D7b^=aR(CCD@k z!*q3H_;LF(k@;9I%AVR3BC%a+taglcAlm$0!kP`2*)7_{mGzsP1|oFy7Fi( z%l4Ix-Pxr@Lh?5D?23{yjD`_}#%fg7c^q~>j@K8~MLGn~&Oeppv3nNaQC(b-OfNl1Z|1d#wmxueVDZDd^tiVbHlVuju(`Nk0aorl|a- znv7rG{=bxF%7yT|P`khz;E}8a|na?re{6cSkreP!UGP&9MQH!cyX^BhRy4*anxgw=8-PD?K0mkQg_FD2^BS*HUpD=RS~xhys7r#$;l_9E!_R7kz1lje zfC}j+Zt&wCFGD0Y;l8dY?^3{LZdXOn`w(03KjFC|RsET%JAEu|s%8nq(!nFT>D8dS4jEyO>?ZxNKdZWzqfsl9*N4Dc(ggosYc?r=zloM+Fu=kd9mDv&~+zV{= zDq;41ixVeLCkIAKjlR#!JTK>!k+;8IhzfeKCXN}h%d=>@yL-K)|RJTVw(Db+Kjk`^wl*Y%&EC(0q7V%tZ!DYFcxPH!}syaSe zKCcB5AQx2#0Evjx4e=#VNc9rFjdK2OP=1dXzk?C>9p|&Jpe}4Fjr~w1kg&<@3LU$> z81^m!WhR1DwlnzU&#GsNk zhtU|O|KXM4)z-KB=+}v*!j6Tn?_t(l8r4Pp_EC0;oT|F@QV{?;Ti{KE;<}j!-Y_*+ zRqsVD_5{+#DdEe|ea|xEPD)s99=Gd^uo7Kx?9w-zg7>8BirYMzMnQZ<1NzU0YB?IT z$3K&HVC~6$kRFLbHx#A0AZ-_u+Vdm5oS7y=dxdgo4z<_ajmx~Ln81Uc%A0lY^M0&P z+g6sYxfQllXYT^@-%fd=Flj{k*s*P^Iajv)C@H+t9Er&}zYs^HFN9UUtqW(KuBrO{ zW>*smXds}3 zXfNSsm&!O%KG`u6!WKdbacsjro-pv`21lN;B%;ElT*3=-3n8FB z*_oN(w?hKO<*yL>SkdA(qz^E?3@^#bO3jOf@%prwR!C)V(ufo!&pz)|dT3Up75bOz zb8)e4b(x2zm66F08R$76i!+!1H4t zhwMMsg3pxcCLEJ9KUFcTe+bdSbU0;-!n1PFbLU1*&8!FAw@fT=TK#e5{K?cF zT173PU|G-C^6B&#bg}4F_@Z{ve|sj#DsDBSs2D#E3ua|m3ltQ0jtL$4Pi-&_kQPyT`7|`WJAH_jj?5Tyw2+z4>+Pxl!Ze*qIFP z{Kfqf^ua6~o%v&)qVYGJ`$jyZF;rZ&3@^WIOIUs)e1r#FC;+=u^TV@v{cI=;Cv)?g z-zB!p4iW<6={pI;vW=kV(32hnMHC$zb%c*AnL7FQu{CNW%&?t8Sl|HZ z4)KNQKC@`H7kLP}OeL5VT|cI%TWPw3IajhcZuxA#&KY$HN~UysTdzEb4@PXix|L4imDBo+ZGvS$yAzdM$96DF3&*WdHXw3Cv^h1|PJ zS>PcoV?=D;bb_B#ctSAWmm?&uLjvKrd4^F#tk@zgBULI9@liFCVt7jo`%qopg}aRj zNMo>8PsJxY1<$ziiZJtK&Hg0E0}I!zd9QnUPBx2~YfbGtnZOS!bzaCU5-YL%10fg- zYq`{FGc#3OR_E(JXSg`(r4^m!%gQ(7D_ysX&Gb!lZW_j?bF2k5RsE`M{e||zy0TLzQA02d$?RD zd@2hyVm{`S0eHf-4ZgtNfhc7y&9gji1GN0PXedz;5n6+FVsb@|?~?J~CJGlNIP6)Q zkS+`-*~H=BUVx#{#~IF-u(b@oc_patf`+2Rq$W3sN5bwb%c*DVfZ*X+emdhtcQnZI zerZFmtC6$$efSqqfzYV0Z6rTug_BFoT&u|hynw=3v!&hVPic9{(rr^5Ul64-+(%Tk z3L{wa&-f*Xf{oQ{NTi=+9>SJB$CJx5+Wq#q8&g0+l8yH27B?D!UOm^4pphTt+aBMQ ze9YBiWC~4iaT83RuTaF~MFe?7Uz1V}G{rSuUo;HjrH`z~k5_ncFF(*dydZY~0rm51 zrqP=Yh{&gRU@!+n#4YI2BYM}4k(J2CN9J=wfVF7fNYrVN&9T9e!%?*Y zz(Mcd%n~g_?FzyOdJ;DUS$TDoE#aw~NTx(8@x8a=(Qz(tWbYwxgx+fMb2vx0XQ$HB zQjr7O`b@`9hUG$kJS2E5X{P;wA=G?ciK5Dlcj3EV5)>_14!4i$-<6co0Dvaz8J=%= zNkoute7|4Fc z+j>8^B2}sr3MdP|Cd)o3ONP>D{$QWMMHvr|pR6S8QZ?n|2>*gbW?cA!+#v*SP+~fb zO`bsjsk{RNekKxOL3JSM<7F?Wq$vY4-NjI>-#ZrVBXN^qe0Nn~+AY+wO#Jhj zTy?p4?%S4uMhu~cWpCmF&t&gOKU#!EVL0{{vD zeK?<0q-=DxR&@s>hC;QUXCZDtN4JJqDDfxwV2e!?He2m>st&}sUj8C0&4eVIM^`g@1iZM-(s*)0k5XXjJtf$1gwjX0>tqGLcH@4li zAB!;DF)$o!kN4(>kplg=cPNQzcc#;MJSAstB+$*9Q^7->*XKGUok}GWiA`h1hARIY z4FQDWBh>y``fvQSy`e_8QOzkEJ~eYI0nXsEkgzc__82h}r(17mR*)0)o}t{R%!thZ zwO0~?4xEJO73SR~GH1Bcux=lW@9NP9=H}s8b9fkyII!#w@m|=?y&$&>v2^2yNx6I$ zNSv^i>b&At{NydVRP;N94cZSKvAkta4I8yDjMAKQ!F+--Pa zw%Ac(apV=5WR@ZBaUD{Eczy26MNRVr6s(o=W16hviXTKnd{E-Z()0YZ$-a?`q?xe} zGa8`KiW8szaI`yLF!nc&AXzcqba2TbpNVTlnP13HI7F)NK*@E`x$cGy7nXS=jk1cb z>fq532>!F?w~Rr_R*|{*etPw#NOL&&bisFwRwSlEQ>zzRzgX1ux=rE^g6Ddvj=z-k z?a^Hzz9(bH3V9G;c93iM_qB)qJ&}SBAzaYn{8Qzt1aSmpEkv|5%9m5_I9vv#08^|I z=Xs;prhrjU8KDZ5ABfTVTR~R3&Xq#xrmm$5acvUf&Mdi?wjFOz6y)JHbA0}EF`J<1 zNp(oB6lJb1bc!IB9rf(psB2VgE>DpeD6U0rEj>2z4S!Sh$X4P(wn+Gb_AZ@!EQzK7 z@|K0GFT9n2>x;;EJE_F#8b*31SDZj5Owue*tGQvdrJO@wY5eKn6|CW7W_zL@DZMnF z{rTqV>Qny=8 zo7E}<)io3x_0as_aqGnE#N46R1r{4!E-H3QfB#+_#{|D(a{9eWxAubE1qM7COm`{R zrjPF+9T|hE1wHYfuq~#Z;uRT8BdsL%TegEzLxYo?s;Dx=eD%XB`MDLg9`i=^xW!tE zTvFr3>bgXu(Zo1AfYNa|dB-WN^X8}R+c0?v^+iwncV?DURL+nPEO@&4NeQu|d-CFG zO#De5{-(Q>Y`F#U<6~5OJcq(n=bm{c>=N@N3d#?aI0P68V$n8SMaDMCDsU`ZzkbYr z6=|V$^pYC~K>rZMwEs2z) zUdjDrc?^!K#2xD#e{^T(s_}gYJU#dseB)BBeyfa$?)4)&%Tf4doB3ID%v&=GQ)i1$ z>8#^87AN+9#`BAr+fYd zqf@-3AZwBC@_4K%MCJUdvnbKMBE{3`aj%eJQ2_Ct=*`#^KVQ*yc=W~#atDyUNd858 z3yoM&d6@VpeK~6Mf~<86Z`=}D2|`>H$yck&5fQx-+xO!sIJgAxG3u^Y()!)gyo##2 zhHt<6ZAbw~j8VjNx&=I-(ym0O2Fm8rw83M#adjT@iR2TvWoLzHBJ*3A%v0AP=g;!~ zVq8R`K*Qb-cnY`1(oKrT2qolFBqQ5lENcB&Yq^O$su#V>Y<;g7rhH3d*@lW02q(w5 z!?=DIM1o#PUS~*5-g}sW@Xs!g*iw6LJm;;eeOQ1b~ zoG*%wKhTKPo$JkHZqAGkqN;_JIF_VB1of@7jki+>Rg z+I}9$PX^fSAAWqVe%n~;R_M6~ zyI)5WgOMa<5tQNNFk>?u$a&h@>n9UI=6$3Gj{WONKCYEFI#0&B?5=qM-~9>rdhvu0 ziis;426&PdUr}WaG)Qh&GJ$>!81O?Mja6w4M?$LtsA1HEO46_tS_|D5$#T#^7eL-5>F3#zzuk$Ex|A{``X!*EeMj zwU1Ofa?!}hCu+=dhc5BKKRPL?smpZ~HriEEUAyByM=!_8i_Cy5K1{qIcaC7Wsx*U3 zFAED?>mdn!A8PKM5^eZH%P5(hqD5qG;yMi(p@tf%ene0zB6jQ2m{nDLf!Gyi+sYjI zvG%?>c%`VZTL4~;3rjoBG7KiW4t=_%~+InyfWlU@8s21?3$H7=wPDz}q+eeo|1lZIg8Y@*D-Il-h z1_P^^Oh-dr`zEJLf&1_)r$x=HP&E16nj|`MTX5(Ny z<_=6tUH>JY9quREplT^J))QfEcR?;L>0=`;^A@l#--w`S;&a9-%qV$5?i3=uuRJrt zB=yW+oQ?()r$*Z#)(S@SZP`n=U*}2|{`jT9lFZ(~hA~NR<)qFt+%`&4*Q+6gIbwe& zq#8}e!wfub>UMYR(=dM5EXHcmHIllw&Z31&kZfiP%sWfIjzV9w2sS@9awsHF4IY)j zWKN{g4eu!1{lSLD$b~jt|B%>Ms>hi1PqPs~Dr;JE$1dKXV7yB5JW-6}*fQ}e_K0eu z{7O^Kd`#4*bWJDPRCyWw*N4NJZ-iWCd(TTnAQW@ls7zWrkShX}@J+MwC!-BcSzgC+ zkC+rO%@%3S7H%AXQeT|ZGO(mTjJdM3QN`)ud8+wTn=~_Dtz`;I1%}q#B(HBU)TtF` zlAfrDzG+pzE@R7Rhc`T*Se}xY-vwCoUCZQbRm4oa*JRmijkT!uKD;2e2a%3YTh#0q zd1kXM$0MODS6AW{cHTxDZbx)5wZhO0XE zZ{lsuDhm1%Gdp8^==?GgA+1c)`wE#riD#bH51#geS4hy9;s*}LxbKc15;w*ie zDiIyyK>BXzGb;qI?9G}s3aLM7kTUkaR~ETAg~7McHeo+{6{?11+uM2yVIuOO26W_( zt{m5LkMWUa$UnHp+((eRQW$yi%XZ-)`uOt1I8)EicDy>FBNr8m_r@#-x{!5EH&WXQ zO;z|yVo^@Wx(uu^jUf*l%&3K156FY4*Hrd6JBVEE8$*PN914zA*ncx3&1nl?so z>4v5z#8zr2dM0G-Zvlk%aDdnH>2v+g=$gsrt)pma$534^<3C8b_kH0wuotW`^yja; zg&-UGZ*F?6dZ8>zPrDQq+QMmQzl2oB5z1Xf8u^&Aj?2SX8Q~IKUCq!^#_DGiScZyD zmef+`a176dG^>saEotV;T$E{!e+bNegDaLeZ9x&GMYfR3M^qUJT?qJRhe85C!Xr3DFuALpTgRk{&4a_X)RumI|4w@DvL7*ZRyys5U`lvOM5a7s+ezAX~XTAnKP?1h~z7AYZc6qX7Xl#c{ad3Y}4u z|B~%BZWT2#h~NK^$aQDZbU4Qiid4AbilYmSgF^2@vIkJfR$WFV0j*XrdGXM06C#Sf z&qWlfti+n>I!bN1izA{)fFr^Vo!ddM-)DP?cf>e-jY=}%yf4U|0SSWYOCl_6Ke=fm z(-C*b&}bH{RB7BV6Od0yh@-v#5@}phEsjWG{!K$I6E}ype;b_IZuEH1lfpvox}w-g zOV+vIDZE^cZ$Lvsi%9kdf`eoJp0Q29Y%wPNzDQxZXzCb`Q%~OY8J4g9<^uDez!sP0 zi9Coma_|KgAOJuzmlKisyi1EfgfQ~EeZ{lrq`8AmJ5joQ`udUl{9i z>3XBv$IAxr=UA5Eoq_#F!)Lu$tQZC}vWT1cf1nl@;aao`d0`=BxAHy%aayst%xi_{U0?;H;Ox{R)9+6f@LeCh+GCLZ>8^ z{tzw2Z>E~7HT@m`i~>2_T@H+j?(cMO_(Kw<{D&T`Oi-4sb0zkX!pY9WpMOiy@^|2r zGhM?Q69u;?c_0WB6cl(_YflXx#h*OxA^7Qw`uE!dq`Tnp)k8NC`oM??bjQDaU*tva zf?z!*awe0l7X^uXMk#cVh0&dL)M4Uhwj*?5I z(Re|-n5xbgr9q^D9G_>#rR*J=S?EK=U+$b<&Y5P*z<7NfR3e8yG!ha*G^O6Xk%LUT zfj&ur88MuJ~uHFTg)-5QBZDbB;<-w7a6>RO@v>`HoiwDA^j zVOgtF69FKfxV+H$)w``f`ZJ+Ui07f$>$^h2HzW)PnV8&rK=oTmUUZRug6$h8 zC_i1lzmg@s<(BxZC85RTGLH&^lagwAG~HyF83kR+AGm<0hi*ln`Snof$uq1M0sR-C zXSM=;K-eMW?}1rWC!|C!|3UvnyV$=Mb%;~e@UE{aJta@_yW*?LQ<`axlv#9(dac@Q z+8G@$p_VAlZF8wI>F{TbeK`qBN|KMwwL1o^qz`cqu4TygH8(7!kQ>L3M~aN`h+%SA zmm~lLC8W+aq-U-0h5)0g*04L*$scNPiyHo9c2M)J6OR9K6S@&M`Z>BD0yO-s;w4LL zrdv9xwF@TPLR+@{2$GPp;3X zK-nhjM+BEZ^EmChHGx1l98))a8m5QqL)~?AzS`s)s;$CA$UO2r8XWUCuj@Q1l!v#jt9V!USvO293{`dye}* zs`yRpU5-n!O30yoXtcqXm=A zeJ=@-Cvcavl>6TDfLPIlwAFPMgk9L80uh(HnGXE`J*6OM5&2r zJMSI#^bx>jf-~JqzKQfMJ4oDVN*ktzeToz60T5Qxvhy#ap=iC}E>1@ELNl!d|EWAE ztH}wJ?cZmOTi55{s(rbyc_Mo~I8iKH3in`uPBl`}HN@;_li=aYXMS;k8;LRKZzvQx zam950cp(pg?ZdqyX;Hq=FXu%XBZxN&OpEa;s&>Z*&wM0 zN0+#~0!ZLITW{D}M!06;?=Fr`?GyY1ecr@#B|`+$4E+X|>=WNgKofO#q-}2w01ytU zTJJ36GOd{H?X4UF(uUk-7$f;~^9YnYz+_+L$!yPca;L4$OJcvh`!fSf^>SC-d!SWC zQm)T3no2w&L2K(Zy|!@{^8|v7(htd>AU!!<#bRexrydQJ_1e%X zfjb(%LvVNNhljLol><3wvCil*fCS)#?YeL0C4ldH5N+j|-#p}8QPhkP--1E{WW(-w zPh@thpcg+=U}2nmk}w-~CoBb_V<6zUZ>#CNwh52fv&mjxBv}kDxhmQ~g)95gnkmn;U81C$BNrq|+w4Qk zy(ki?|cP}A}#<fdh3!wGHtK=7=qvBOz0Uoi;m_3c|Aha^EB}URt@Wd@=5Y5D2AbUR zZNz0V2Xb7+tv?4w_-Vm{kOT_3w#Kp6Gv-+&(1!{H_mkX{N(LDL&|X#+PxI zA*Lo$&o$s+GUuE&*^%VjecZbh7{`4AQsyO~o6vUZi94p<5)$3vwF(C_>Z72~(7hdr$0nBZ5|~xi;qeo2f611_Jl}L}Go&jE25f z{mBS|RmD+%s`I&8)F_|{H4pX`R*<~^VExGVA z{H-R|cpoC|LF(hLv=)?J)(xGh=$3}@tgRG8`yTqBG)O;=Pl&v!QAQK6gFzq=JwiQBe;w$_;GzM2w_ffq$Y70=84<%5F?denM^2--n%^)IDr4(?F7 z*p3kI`%ib#l1fvyn|G{#qf~SSML((4lt?wcRRV#u9a=yfmtW@LQimi(zYZjP7Bm}; zNSTiE#q1)$b_yw>bGw*4Z~Of!Kq|A@^(9Fr$ZU`HdKxl)4f;N5A*0{c)>YCEnBeSP zerL>xK_QQ$LFsbu7Ua5G4_4k72&IlwG$Dgan$xnriS==$%o(+|jlp~9&Q<)e{zK72 zEigC|AfE;Rdw?Vr{ozTrh8ey)5CwW-wsZu=aedOX8J<*k>!UO{Z1`@Jwt57R{cH|9 zA-N*kya6#cmg$97s;z0(!D{fDA;B2xdhLcW}EMXm?dmK8@!{W)0X(Xh7kj~TwO4ZIo{ zsg~~az+t2JH3^&dR-co(E+xS31-T`}pc+ZTIZQS_W}APGaMGL+Gh#OEd@9p9iSLi# z!`tAnCb&1}4dwLKqYi(S)zmdCD5d87fk5&OZGH;!8eK}|dL9E?00_SsJ2(9B5DVvw z4IaR3M3%h#?p8FN{#(L=`eChRYake5=FY2(Ad+I(9OC^IN(&pg2ZEH{FUMQ&aXlFagF7$5l9eiwf9a4k@%{W0IXTP6(AW=rRhrkn|FpJ zXD45Cr1{>_H3!V;>C1D!KnCsmIxn)*3N3;DO&s72WQJo1$;4Wf-NIKcnQK7w3xj^| zJdo&J)?xFh0fy3`YZe}Vnr~l_J468G2rSPWQOdp(j*E3FJn--Xk)QChSB` zQ@N!lr|sFvh|8Djd@i-gk6;st$emVFR2A3Nb3M` z=S=8^1WGW-ws$E&9|G-Go>?jD0g#f=M=cXgDM=JiRRwv0f7ZM>d^)e(swB>G^uf_* zFa2Dt5B%pi+!@b9#H7vc&v7rv9Yf&BGz}g0sdRY?eVqV14Xl&n2kdc;t~@oZNV%Ibb@W|vNl<_m$g^*{iEgp5dEAQS0tkW*hE zi`o6-AnZxa;-n_nmO1LCm%% zI{9p?v1eVkDEauwLSPLitWvm_1ki+?0Gh zM_kiBZTz9&hQu(tE$+vEO2KC#eUBO|+8V6hJhCSA%yfNGBv2bA1})7&)XH)ALdQ7n zoE3tLV&4Zg8xizW-N3bmbNz)jUTa)Lari+5GnY%K z&5y;pai*61L44j7q?d)x9kaQmTstF38_zXA6E_5EP&s?0CHF?pEk9H)dAW`BtfaZm zzPlBI-UjEtDTQpl=GT+q7GfiY{XOA;BqUH95Lh#FcduaQ9lo*==osVT>n)|8%y9K3 zzzad9i(OHP*-Ks-`sBf9PJ_q-COq}flk3=<{FTVt)cky@zFcqYjB8Eo;KqKkF8YR7 zCjQ|qbCdC%0!y`+ibgZ-f5?SM4@wznbFzB!Fqnu@^u3Ovd9qRLD4HI^t>W`QrNj7# z#vjhOB*PM~^Y8g$-Uo{;6_yQmB{aRdM%2Y1Mg~jLz@AMTeQ+OeJsbqe=c_wP;1;a{ zaAfeumtn?-5Up!+M!hpy2{mFwiVNqClCYxVSO!TyVFcetmLMZ^<(oa zGAq^o?l6wmbD)lipr2gRM?t6uS)J8e#czaY*31Z=o@5iJqsm#BrG5V@(0U}#m^TMX zbq;s01X-EU(JI5W_|pYoVS{O2zV5R_&~PRmkujN>KG(-YpWagr@ZJy$2rw-+7n%*# zf(C<+H}Ls1L0w};y0E#4js6xL?fxHG%9tEuNZXbN@A{}Owt1#{q%2W8UaN+|#u@1q z@`0aKwa*I9E0a-91uv)EF#@QY7Zx}vPi|=yH9{u+$Rap9RT6E(HDq>sL@vwe01|+C zbik@ELXH0uT3bd1?1eO4xzH8!uK?$k7H9HoK$JDX{Cv^DNnf4aQ7su9ViQx0Wkt{N`qGGUa|5O$_IT`wmH3 zi_U_#BDUT2ay54|f0vVp5ZaL;XacA$5@;BdhjM@57k|?1;;A~^e^n#YbP)pCiZAYT zGOX1 zpRBXa+B0+Kp4r#V_MdF%gWx!^i(gGF@4Tdv_~R&q=0XjO5ei(-wng|E&guo;%T zyq0K6Al?++WtE=9Z{bQAh;~vjlKPb{#*o{D(kJ^KcGHu#{K0q%6qHGrh|~|ks-I(B z@|&d3`*3l1a8d9%NYF#IInv)i<{80}qN?WtXMt~;H@qFzsbCnqqb-^ia^zuvP`WgIq9`Jm zkQsjBJg#*hOrOuv$h#r_dvVY^^zWSOn#VFU>_JXSP?S=%rleJMy~f-?c-1sQu_f1X ztDRNH)a5#p2WQH`4iqou-ULL#^N0+RK!O_0RmjLn&n_ew%K0dYw1EUal-lRo{cu{N zRSC$ZIuvtB0#|n@&LMI>#)-yItyZZ46)Z#j#(X{hunYnK1UZ{wzcJy*hg0h%d_;lQ z?k)&VP72FU70O!n4?x;YT|KP=nMStnMVz@cMOgzAU-}Z5=A*^hKD_3=s@w$g%J!Ic zrYsh&BQ!b$Sq_6y00@L)KSq;dFk6)9nvtVXtN4~b|0MhQ4(bXBXf=>yft+Sr4+_rZ ziM~s(lL$fe1mMtg^aQ`VnjwIplEDBl?e_m2=@{w=1TM*=h8gb|Nz)pVr2n;>waLC= zCcjV~47Gc0o`gJlApeV1sHJdBO1~2w$D6l!iXL!45haUar@&H@Fhl9Aq7kKBX#0*` z?rrC+SM~Lh&NdCR-sQ9j+r+(qmVNb?O6c$%x{E&&Qg^Tvz0g*4ua_kZ-!*30qfe_7hnnV;}&p0LYkIPoq$E&~n$}xY{w8)rjju9nh;4EKj`w>*l*2wC8CeI=ssIC3Tg8op3 zn2IJIu2^Y<`e6?a3@AQo$I}l$4GkBX|8+?qLQonLa5bnR_~B@qu!Rlc!(2p-t?QX8 zY?L+pz`bini5&e7QP$5LhFp072v3uognm9}ib=T9fI>=M#_s7E)a0sGi(%^6v?%o% z9X`JwBSmILo8^sIj>hW0SVh8#QeD@qz`hl>&lbW1bxzn|*Z}IS)|!%C$TYB|EM3Ey zP{y^`0wGy)jx|V2=U{e7=D0GnkpMEc58-{+QJ0y_*xU>Yqy%OqUy(PdhLC0|Nk#C; zXUqAAbN1zX1pYiY?MNbtK#TE-_s}`Q-g~zxP4Q``Wu+eo#LL;lG(JQG-loOeb1arO zX84Jjx?ea153mqM0e8U~mxs@aBzXyWJxt z%`OGqZG{YJ)KoJMLvY)Rd_tnCgJFXfR{*RMDjafdz|V|L`Oy6mI;!vHOn8bC5m+0t zXy!$5AIVXc>muh6%^-X0ni?`mQ~=ekc9NZpHO-M0?4}{O@yMSO%MCu$BdD*4>o#UP zFA@97WQv6P4LQOT|0MSt0EZx9aC(AwEMkj>`j1F5gAb*)+Wnoyb>X3mV`7;Z`*C22 za4A+`7VGc)$@u0aPVZF9{}LYWFXZo!r{eBT+C8^QSQ2*=m^ARWcht3)-7&HpW(hsO>E+L>Ti^KI`SE#U zJxu79RdvPFoXP$mtQzrNV`|=yhY?jE5^I>JlCbey+eUx!^=t>rZC3$BS?;vpOGI zZi7-(I8oVmwjCxT>@WHrwcxbBXD6T^QRP>B4b@eFo&w+zk6w@h5Ea=@$ivKLFMsmV zLbM2VAPHP`$I<>=*oleiW)GamYS&3ryLCo&gzdK+U@AerJty?3N#F_2m&d^Uby1e^ zrOs^j^hNK=yuF+B4a}<6DJTjia4_c8@2r)glya{3{WKiO08z zI(xY~qnMw7SYcD)zbqF7TgkJsh|a0QYkkfNKS&srzpA6cF#%jAspc_{a;HJ-j~vDu z<{A+^v3%Cpd~Pk&zLI+)YYnkJ`8+#h$jDwJxWW=Z1Ar+Fao%){*w;Cd;HM za!KTwZhKv&IjtTr6mQ{ixRFi_d!^%;ltP_l$wG$n7Bt53I6|B{wX#U2FTE10wPTCU zs&Vl?G&!M0P9=egmL#CoOEn3&eGc@zDUIQlj)#~=@Q%B<$@DZqjJ5bCM9Bl#IWWC5 zLo1dK^f4#JT>y!2a9YJ5aE+XpI{;RG?ZJV}g{}Q33k(Rib4&jk>@62WR~K_QCsxRD zb7rBFue#}(V0`^A%K@aar1iQGS-g2eP+#$=!=rDPWn4s&<`zkuPBgNPorP}4>Tl%n z+v0_z^7kzwS2&5svp@f*sv1gn)as-+$$6b_57ZtIpecIT3uBA)Xk~3;JIEo6< zHV7B5sHr@A=oN2kK<}qP9uonxkV9V~&~*=9MS+p14cFtrql4-^GVqR<9rEXk;iE}v z-?4hm)B6PMU!3q@ZVgbt{F8fIRzeuW@p_W3T)70^IMMQ?A zi{<(v-!r?iB}%A-T}xdI6=Fw*`%12Ko-Zk3<7DUTQlk0t2@qgidwVUzbFLFV>L@xAl*ZL0U$oQYV8ah2#x4IVi2=uhD)d@t3nHAWvk&pmG z^fVs3a!88~p-j#>PBz4Iy)QKNcY^2J>LOsD6rh+dy0KdB`z)gjW4kwT{Y zzfQ;8R(Ju2Q@405ZlfGzImiIY)TMcFy>OaZtBf|#q>U1GW1u!92R#JlEqNi6yTrda$b0k#^8gZ_+hR6Ot4J#J} z{(`=~3b{Am?K>LEc3VT(|JI^pSS~w4jClJ|5#Xa;vhg`W%#J!8WolcqvF z4fKl2ICvxsCs@MpYMM7|$ssc`!?!svF2P)?&Ux)&vwqUn96*g?J-Q`D%F40c<(65= zmG{8^#W&!xP_6Iw?5O~`R1#)_8X&xJVQI^Q*QdKWc{V$tlHNgdF3$&ukdct1!s)~f zR9ju>k2tS~*Ih$7HIKlp3Ia-=NMil?wYY9<95buJ!P`4q(2NUurg3U$m*HiPoUp-h z;CX3Pme=Xf_$Rre2-qy11#T9p08VQreVQSbXqIIO=S)rD6^TtUvQx7layA45Vu5z zXMY%)v91wb18OQ>j&~#=ABi@fib@)%eA*|y2(3$CRT$JJQ~7C~p*%Tbv4|#Ev9|tJ_RQ2{4Sl(+$)@ zqhK)5fdB7y=?9e5$}>-j#i_#z_39(TCP%-l3OUE71NKNNXw0)}9R&^w7QX!)d!{#Y z@^bslQo36bxKlT>>OH^CCPpB^CK{vk4vP|7=wXw@SVqUv$sEuXlG^x2b6MuEvn7$J zLx>}nkbJbY)jTX|qOScKJS;k)y5~>BupVJ8M5AinW=#jfcw9cN-`^L-^M~!DW?CgC zoX);I7e@1-6gRa9!oudo4&p&xJt~H3d$*<)wwLIA^Rv2(GV>2{UuQ_`D(o;*eJg9q zTbU>N8#qhVDx>Eziil*N`Gw5fgZ~V zlS5rL*&=`5#Zng{^~P@EW%VX`E^jp*0F~3!fV+%(oY-r*TCd*!YN`n>L8;ZsF14_R zs#XW+`O)~Fa=7Qk5t(;-=SrHgjp8(wSUOf%2udK$TrCVpr`GJQIH&--CZ6O>>y&X$6(mD zbi=fOGm*vJ9IpOMKa?V1_nfIY{yhaIY91c%3O4RZBqlU&(y>gn>u|*Mj4@)mrcf#8 zN&|KdP@m!%T>R>GGiBgTm_C4!ZK~XV?HTE{`+*nNO=Z=Ko9js6?`h>ql7D8cS>VX3Lz=|gF;fuVo0=HL zB?l6a++CD9^TjT!_qA)MwYROxA#a*sKl{4hUXdQ8MR=aZ$I1KC32yVoX=cxSHLUZ% z(dBuiPFC(;Vch~tIf-aB91Fj3hi4HG*o`b>1w-1X}6eIpv<#hpSb&JXxnpBXubl5p~=o?K4>7KGBMHV^56F% zQ73?+sbI>nK`2Br$wLJk2XnAYxgV9&C8`fT6%0my<8`rg|Z>@-crTeJe$id-AeN;j6hEbjZjwO{s#CKm)91zYwDk zQkU!8PW9jFvs19t6R&DEy7Zu+G>IWPwa+Bcx+WcQ;;)4)J7nkt|G8Gqmv?n66-qBB z;dWbI*fBOPr#?Mr9C&aceRs}_ci0HOh3{GhWT*OjE~IQ`nN(tv)-nr=qYDIaX0&!( zhgm#=wGn>);Ih?dRHX_X4lgWRYCO=95r+F)H?_8@StQq+j3Y}|S&e8D5-dHE z5n^t2RBY5;ghLxwa_E0DM2`Jxs$M)5S5uauN>zn#e)t>Dzf!dSmQMcacg8D{@)@EX zbJYm)>)f#%FR_z+l&~Z`08gqJ>z=i~)5b}?+ILX$_UKggwdpAfU{q>}B0uuE9b)#% z4rvXn&agnuZdkQ4c5H@S>H{xKk(nbJ^`7eKjj8bTkTm47{U!&7y+a9yzpEbCtJ1C> zv*GD+9oqS?JitqR<(=#J!F~9@5i6Ct z;#0PVyd!@$UY{v+;E{wOxpy^o&!j4^wy}FnTOUn_1t_}C!5X2GYXdR+V0Cm*BpE=7 z<+ffyNEB1Y?D+R{2_tlHD?jsc5L_k)r2VZj-SGj0bl}fD!tuC@QgPaO5r0fV^UG9P zMI6j@{0q*j9TL?xehPtwkd&NJKF?lyu2p1`)=bTDd<-Ylq*^-%t8;bu495JSnpVlc z=VDx4B!4hAn}2&2GMx?{if!ZBl0aen0PxbUlJ$_7JhAQZ%(E^>xsc2muEy7sS2WX% z`jHal&Q*bbU3P;dt8109@6%Zwf|yxV>vpKcA1Ie6C^*OvHDT)OOZ%%L&0yjrW86y zS3K?d<5S?iC_>&6Uq`Bs>&^oRR~O4~#EH!O{NNy}Wxu5aOp*!q5l|eqK~#m@8)h6@ z*{cW=P0L(Nm(@>@JwCEuPddh}QLgTnye%^U(ysDbgyZOvOgyu!7DB})etq|**c&Xg z)umsItpf?AnFks=a> zq7;PhLm7Y;5;5!TIYggz@fjeC`CwXp00R0(;ZD z{6sO*a>Xq?ks2WfeifFMj$;BC)$c*%r+mO@8Knk5ipHRV`R8pK%?jJ@)zr{WZkM1#QI}#fnLkXm1V^C;FWOR+VW@K&BdfkK%i$0hCC2`mE;TQk6@Aa88bq(s z2T}8Eh<7ffQ~hPxp#g*amBePCG{V-0U6T?9%#AGDzNr1}tPT(1y)qsP`c1sax2q4B zaicfUJJlVBIl=O^Gn4v4Js;M2Is>P@=im`K5%9^(lZNx3V=APoHd~7$(@q#hLixg!`M4}7i)oq4XOzV9_#_zfBRV`-*`^L& zWfnvNvYB9HGlN@k2OAj}Sp}aAi&51RsJ(2s$%(vSsQGLWK!9}&7CmY1VQNr9BEiF` zd4Xl%-OmrkjYD2YO9PP#8%c`1Kr~7Zj53{BsX3ZQDRkX@pu<-=c_t!|*jyob9SEEE zQUprihqhSJ;gEQtFV04T8?llQ>eY{IUKg>B<&{_>u^ms0O60#cmm)IKf}u#Hm{FROZoT2ONmeT52y;aO2lj?mL?NB8oLOiJV`noa_85?&SzHW5oB!$1aXXp-{hl8ooAZM<#xxg5VgAFA=J(nL^< zcsYOA6$cT)>-7DO#|W`o1G0F*?3t;_!AG|^cwzJ741>zQdh)=Dhrn~`I4E{=df1=l zTU*g00{&$=sO3BqYSyP#H4sD0GDN3~pG@_wt-+cV&tD-bN~W<9e~YuPocRWj?#ubmV)IiemFLu_hJMB_rpSvej{q z0a0zOttMtO(%fLHnXOrRehkn#aBa}APO&nKSvrpsfZ}%$TdR)Q=V&B8_J9uBrrT%(HBcmEnmA&=jES0 zNK8zg7%|3Sl8+<+WdN=e;~xjUxha|67Wf60w}qIOmDL3^1)-l$RUHz$oSvI`X**h5 z*uJmMj&xBF_ZIYegW+WH4f3roCJYDH0$_AFG*R^#k_HsYNu+5$&cj3(jiAY9#6(ic zcxZ}X3wQjJ+yMaAkyl$`NG5TqgB_{*5 z;CYV`6uQoefGxNmAkwv#CaXf{+e*aQ*uDt_J+)y#i6cjOO5 z4r}Qc?jsd$)qk^7ofV$Sp$EA?^^~n0Ilbjg1RS?yN+&-vg|?Y1eL?+)*#N)O1%f$? zwNN#s8YJ?ltY<7cNAEI9`M*bmPj|5M818jLd~-rK9X5ptr)P#7#LU=tr*d7KGS3yd zJ0NvVH}7^oVY~>VFfknc`O?sKRRW;$MOZDi-7dl(IzpbtLXH}vwA`zgQK34I?ySghtddhq zIV}aLP&hSo4jnwWA@!X&L{@`Ou40kMG6l^BU!DevB)KLjL$`>^2rye#Gd88aW#DC@ z1e>=nUaVw!Wil6CRn=88&;1@%EoGjp!lClY2EZQzXth-sxWNO#pEXf&kRtfO1C3mQ z<04Z-nfkjRhX^dIo z834Qw_xzs})Fn z&On zxalqE`HI)%{k7u&M=&~kwnGdlfGbLIfySD%niwk@Hp^YS=)7k`p__a8>@X!qXIV=u zl8D)#UIFbgCs_jO>+`MJIn=sX}wcNJqi+cS=M#z!AY!P2(IV`JyErOVj@mj^;KbeKs68 zI82$Pf08={NSQ-VBtfkC+!hn}$aFxZ8FAerJU@ZcEPEC6C;?keftZ(>zco4(@8sqi z4-*F`B`RDZa8NLC$npo;b8h(oH zg2(7b8xfx=;7N7|+$!rz!()r1IMd-ltCm#t7=u}lQ za8cJ==?Ti)gU7Rmbs%}zq%O1Q4a0VYL7v-1a~Qk8=(}zmu@rPOY;X_sNW9QwITunX zRP?TL3IN0Fgy}Zqf1a}V$Wvu?mE@Z6l#>gCEM?n!$R?GaQA~6C`wfMj+P(*Xyo1|B z_s<#UZha20Y!uO+pwkJE9?cR&r>}~tzR{P#!2Vl~^mBj(HLl zySenf_A36|^sZIcLJvQ(ip=2J5!j9mZBV1bh~Dl*n~n>iU{Jy!Si_nK3MW_+oshTA1QW7cp^Q5QiMc2*JH&2G5q8i9u*2+ z5*rdrRl8M3w9a83#pkxIkIcP^`8DMO$z&YG;eIQ8cb+xYHFD_N9a!6E)kJ9As6a{^ zt^cwdJghIzJaIS~2NPXZCOUO|45O&RF?##j0m_NBb*7@Vx0f&OX_fI^^|NbbrJXI6 z{Xn>JloXrfaE|aXRSvjv4PH!7o?6K;V;Hh>b_)3B=opt5@sJHf3Brcow*mD&*herc zMaYhP!OGu>W)V_lKyPdcno**qNhwre$o&csyrK~TJdoXK@=K$9=DoY8sIRUzOvZG3 z8r7NJevki2BgH(adXhQcUM*_KFH@@cAbAOd+ch0ja^g`LfgT(r4Hr+2k6+_a4j^)6 z54W6dRgy|%3bBI44d$UVo;7tFMP;fZ+91ZlNL^R{et#3l`P>$m(}{hzzi@t29+*!J zj{pk>DXw3R2M13ROlkI)P+ya}Pu|Hhv{|Lsbr7h~P}ZcJU-wkw1s!@*vgJ9zg;5(T z{gd1gSW1UybvZ8eL!DB-62&GEPipqP%pK0B9IJ8~L!yQysxo z?Je>fIfmoMWyRrf$CHl`5FQ36bdN&;EG zteEDg2%f7(Wl>y2UyN14kyNDFP?8{U01{D=XDiLCgd}S$H;Nh&YrIOrVoto2er9ZA zScn_4wWS%oBqz#c>{e65>y%pWZp?Nob($xjFx7|ztG2tLE5y+Db~jimSvW!}*Y*`l^Xl&LWY1-?V^K|s{mqtUaq$fWSlw$EnJo9dO$r_Yl! ztN0oZ0t2@hE1@)_pO8ma-$x)<@-Z8E5~K$+GS^)(5zXiV>@tdfjG{ zDDPT-|NP-3Ss8CNmh!V!L?=Gba?-#Caj5azgYMTBu)0DT7Scz1pA_r!#f0E&t(+*7 z2#U_7dXi#(X%MeU!zAngZhv#69<4u$Fa+`{xC?su!_0l3Ns&Fm_06{N8qSpWA zSHZo8J+3)EcVU7j&}F^sRGbUuQgr*{M6LZjZ=pbPV2u)q>F|3r14A(_GDZVg->n8} zV}n+&A?)kWRApxnO^OICE82qu3(tSh6)En~huYK?b!H2PAT|U-G~5 z0J{x^;0$qH@mh5}q7Sj5evwogf|iHntjc1_wA$*IdmZ&y$Bl7`oXyq8^Ez2o`5?Wq zKpP|Uu>C!xTGQLsa3Du1X2= zBR2EK8%W&VMq{4gU&_~iqC*%J{z|>SuBr91R!+x`U*ZK*@?cvm>~Wh90;6hYT;gcP z2Rvd)L-vt**^JkCW-YGfHS}%!mMe1O!6XRjK ziIL#olM1q8l_pg2Ina)6H#rY>Sa#|4XmTfMdE~01sc-%C4QC;3p0r60WY{OV`6sy( zuvCDi4#xl~x&T!cKm4E!>aS-_8Vr2j^T={^ZVX_0i38)9f{cv}KcU&%^yD{}6G3n0 zW!%HyjnN085TAJzw5)6fv@+2WDV&Lc@dG`s(d=fp29^O!uM=kxKMD@x>X{5@Zfm-J z*IzdvwESFyFi6eKbu3n!h!GY*L&q3BXalTE?@?@2=@tCpQ=yR$_<2KMaf?ykCDIC? z`uT5Dsba%Nv zrE#PF8GMz3!MRkV>NDkdneqD&20HHQ zj(>w(zx5;j#uM{X>8!4x!B#~53U3gkfA+k;OH4F|Tc={H+y+3)#*`qA`S~$3^mq=QsTRaENEe^ucFS-eb;ao8nrJY+q(^XZmAamK#mmdp z*jw_@T5;J$zn=;q*x3vAHHSw=lTT_y!BfWBE9i+!YVSlX85#SDzI<0+%u{VQHvLrW zxq24Ex(t^xq!EQO!G+q2T~F6RwZNOw?ih+7ow94N`$q$N^e2F;rDL~Vdi3mzM0ex1 z7IZX|iof0o{D)Yvy&QUkCy3##ifSe@s1hIj+{5K95EFUq+_P$ttfC&cI0H75~d z#bnAJ>|yiNiKgk7@d`=@jJC$o>1oh6WZ>!UD&%2Fb{T@(qM}d8>(%zOC9qqN!$Zm%F5by9;TI1#iR-@ES^CpV24vR#0*bUSp$V^o%{;xx5cz$*Mta!~Hl z1@CNTh^&?*edZG-XcVP|U7aT8`fm@3y$MIuM^O)vmAGZi=@OL(7rX&M$^=Tw2SpP% zuRH#c9;RgoX^pX_uGMI=Fow&mcpwmZF$X|Z`!HNw!x~xE0z)v?m~|&yF1(_vbd|33 ziBoKai9F!}(#XE}770U;Y*xtOu{tzBT?zfx9z3k%>C+YrC|@+8BI~|-O-E783dg{6 z8vaP6SiM*U{3TMRGxK+$6i$J2zdr0h8YT&)V`K%mC{iMkjdb0C#x*N}Q0dYGiEFOk#dtA|49xyz~81k z&k03+3CL9}5K*_I@iMC{@c;z2Ny=NpDFKTTe`=3oXBo3DRqZQc2r_x(R)VXR>r&Rf zDd&f~L2}+Qlf{KT+_Eu-^8f%S2|2w~PCj?z=fcWOzFLyK-_UAQ!PAyNY@j%!T#ES- zW;64erpk^gi-`OyEp>Neifp#EE*7@IL1C>Y|LhfP6aD2Y4wFF7H3aemn$wiNU<>V3 z7ev7GjUV#%uZR9afn@+_kV59$X~RKYL;XZ>2gqrkJsC9C3z=U^-1-U?QpF($mq)d0KV^@Wh^Lq-p-!l_SFeT+@ z2`x%PH4lXmvE5KQx8j((%?jAk)Tfd{nv0Nv06fG1I! zC9stM=p#X~h|K_WS_>=FU@%b;`~`m+C4fyOmX6UM`podv01kwkVwjJGlnIkuqs-ND zOvjtsN^XF8jznZL)4nD}1gO_+QV5Fw1SDIR!Jf9YuEiu=qT5IeI@1L&Ad|+**ma*B z0>EE|$fhZGoP30K9Dnl*P8I`GWIo1>;>an zAIpxBwK_=hxLf%jTM`N!fry&K>bz(SOx$a$N|4ZZiW54zGUdXqK!`3CJmBL#o(RW5 z;Q=+pnll&>gM_#_TS7!B4Tm(Cey&Lu_maKV1i6BPB8k$7QXt1O@1Vcdj{wq6aSb+e zpYer-soxJPGTWEOo*NZ&nN4}7st~s;fz|xRf1nq)@?yOss1kUW*rJ6LC6e8s#N!th zXB1dWDOSFHZY*5b{_zW0^>LN>TV9K%L6X5ijGz(p1_ln=E=T_-xmGZ!U1M|p7`K;c zkhBIcL>&F9Z2Lo`geV`G?UoW_^xOU5nnm8p0p@L%tck2wx;iGQ>bG?p$d0ZpRv_e# zuSNp4IHrX1dGs8+<;Vb`LrOt0G08@<%mI6|$q;BRCrDX1lp*|(%IS*8G4XVDvXO~c z)QG+L$Eg_mgU$zn=v zl)Df+<8(3BDe9X%UyVjc@A_+Dn5hyBhV`$POaEu3!;MXyYVTK2AH&W0L6R zXxNu%;l;k0TaH3wd9yy4fiet47Y%`2DG^nYPcjw9AIv=0l=lMwm;gLYv2tR(Bob^K z74?_f@4Iw(9w>p3WlKu5z5I|~KYs=w;d6sT4S_L75 zH>yG0OSnav84RA6)#t8sTMjBo=`6$5b4U5>EQuweSX5VE;OW4hnG z`O4D|L+;FcAP_c*fnCUC$q~%kly=&+l3Y=(bwaEZ%#Fepyi#uOtb@kj=7lIMefrNn z$T6v$sph>F^#j z=k^1>QkGpH_YXuA2F{h4J#|5=kFMS3lkp^RkSLm!_#>+XVv=+5(R7SY5|)KLRxwEC zuUHO>E(Up{jyAywHE4V72O{3a`mHXIH=3H_Q`vvm);U1%mX_&h7@ioJ-R5U9xUQ&W z&7dOWBK(Y>p{kJG*Z)YBhm@l?tXS&CE(w%+Lp=yXo;aaJIkm@Mi zUjW9@Ni}rd?C&q1N6q!49t%yr#SyHsEBbv0HcCF5Y!;{yBI}ndep{ROJ3O6FJAh1S zCmaz7>=!eJsziUtAJ7mYi{ElPcD$|Jy0tcg?8r87z*5PNL#OvK3kK%6CH$8) z@MLe_K-R8?>-doJLyrQrskgzb$k#x~GkIiZ2vTJ^GJ+sYMVHU~I<8E0upu@jYuxAn z$as1^_8g8O$@YWdES}n{_@5D$sw&!xlc4CgGLj$vB)10+t`OUur(^z1YuB}n3|kg$ zqwFT1QT?~~>)AL9P5x3rys)XN*_@%8owZ_#Qc``<*CBGeE3Y=lQ-z8svJ`>9xI0?@ zA43qWGXN|h$(Q(1Q@>(KMnQ{+b``SSX(UquX}rIyNcoOTzUuh_y5RA#VE9w&t2c)6 z0xoW>RwNjpX2_hh?ZvF(F`I_nY(bXzlR}&4QwojdQp4|>2Y$QRg09kT!bS#C>nOY?;JidmLv91B(eZC?FT zDP_&C`ERw&8Yr{d!xDoc#9<+fvQ$aC^J~RjYVtf6>t|D}PmtFRHR%B^>fwAo&frrQ zXi$J9VYd*S$#5G+VXtLG@zO=_zXle)txCA^K5E}qlmt`ikRpIE^BN^Su241;J63`L zFpZPx7>=V|yG}aiiJ@&4aBGDXZGZRbG+aFN-G(yXco9x;VapgmFquEk`Fi_XbNiL@;MM1PYBF zz%r!{!nPag%u>C~2W1x_mg|81bD{`oycWFM^tDOo!@wYekoSlgL)@|=Dj)ITqxq(K zC+H3b&_BA~7^-@|FyHO`N#W=cUn+}6$ccwMuOsH8*7ts5xQptgu)d>_4$@=Sv>0SL z(+~bugw4ZiW$BN}@zbg0G`{_FO(EMib*SYWx-%{Uwp8745gB)sM=CoO0mK_MqHMpv zSHjVYzxzm*o2J|M} z93>$ITh0<#;05051%C6DVuJw?9Hx>n>heTz&ws*(yuCuIj7c0|@KXTplf*~r`ZzSY za5KPdJOnc`vf;oT(&T$(FmH{bxBN4ocZ~lO0 zF!Q8S)5W0_o7V$>2GT)Ks?_s1zF&wzF!)F(p-6}ag2mv3=668Z^@<*oV93*3x)dg* zIgAQ^KwhN)9OQ5S@R37c(mW^yc2xz_Xl*2z!`@W{HSC%r)IyNCsp++L@||qW1X1j< zkC1$8h%duUnMv6&Cg_C_cq5qx`=3|;967jLZN1?KO8^&2wgU`^fAUn^@vyu6*9xlR zG=F~n+U&7fXhbBHn>b?{%2%012;{-%rT^zd+|g)|EErWx1*;zhA%7^u48M0eO%`(gd`D-yE9D+b$&kfnz{R<8Pke3s+22=KH(g5&U7ql=F!X3m;V3{az zTES;0bB7r#%Qbfmr3gzUL#pYG4-dq5k#z8BVqSuI_jYS4OE>4PS6u34oVz#@nQ;w! zu;xL)_D-3^P1A1y<9=#~0=x53b82>YBXVWDh6})fRAl%B1_tr&i;y@?xlKrp#7EX; z9#sgtoM~?nr(tC|u+*xF9RgW}yj{JC1a`e6A}vt}O;X$EAUM{NnhM^hDs(sqNy&!9%2CiCgBnbE zhAejAwF-GXo;ZUp715smZ6ku|uGY%(eu)W8ix-{!*ZyLqm-FSXe&o$4GHb(8sa+V$ z>a?#LE6&0Hv2+$}QE*KdUY71o>5^`c?(Xhxq)Ta#?yjY~k(O>m8l+RYJEUR1^}W8o zaIWXf%$YNDKe%>q8P0)$U{*z9D8M5ItQgc?bm(DU;k@&B(=i#|uu$t#9I@z7`7!LT z>Mn_Q(LKrc)}1`?(^K*Xk1+tPOD7qII&YNJsQ{T73^`n)A*WC(fhhn=G>DdTvG+qH zLYUcg1cUgkRKa3!A+)sgC)vnv*1h8sl%XZ$){CEa!C+QTdA>8Rl}fm+4|CMPc}H`+$jK>Ud`|~EzUb9B4Y*u@b{?RM+a*dwZu~B z^CJKX&^(9<;Qny(ra@NqMCJF_ikpp0^={YQ0A+&oVDzWk{b(>XgC^(G;KDI z3D|bGR(IhQ3t%a|3?ExYZ=RLV-kKrl9T@|l`0>S4GAH7zHn^mgJFA-YuXEap)X`Qj zi@{0KTs2sfo8MR2zJQCP0FQbUP}x{XWLm_wMOD1_lLvl&ZP+6Mv%6oryb$`}fFHa_ zREaJrj=gvG+0)`s$7UKcuQ32Z;CHjU1VHEO81Nl&dvF)8LFNh~krBBA#ZomMb3)GV z$WekE*{*>h?DJpab)cl19ELO)0>R4OsX@8gDI8_K`v>ZwqSWV?u3cvl4S<|)K_7g# zmGmrMkLnuJYka`ffhw=q!~>vKMk)?OswAehsi{rJUNE?g_Z%nApR_s2GEWHPqa9)DlR1?UrJt$iEAZ4f$ID@Jm$y|S+7LHBd_DR;;{J02+Ye( z*b{KYXL!uQBY!j<%Z44%tA4z2#5Fme zQ)+!L0&AVhj}}0=y;#+Ru~tC2G^D`5;4>E%erB?cEz9t2Vn~)ta{A`i5-W9&j6;`l zdjTv%D?S@*sgj5I)e5>BNyeI-{*xS}lna$#Ke8o*`$Q|?PhgG~ZKUTc8C$D*UFr;> z9CX+Ve?E(maQS3ZXtMG~;Lhltd`)}$@iP_9B;3O#DjgGUkkUqKGs=h*V`pW;PMIV* z=R0N*1%>QpIrxQ<`%!nN2MmF@(F&O%3{prF%I*%&U=q*Qq4N|$*(NUD5k)azls`8(dzmLzY#+$N1xepn^&7*Hb7gab9vjhHHLiWV~XV7zdOo zM*bd0tkiSBSy=6IO*WdZd}L!QPbJvoY!L#vB`!&hmSa=Hy=q#0L(1q0X^wp<|v- zQaY(3l5+etJTdY|{_|;)MhnVbQLNLG@Bn#;0~8|mfkk$bSca|c9HG*0(_)q> z%Z@eu5-iPuOW%3^MB_9*-x=7_qXRy@mu)e^9aCB_sImlpUl+8b!=XXpmc}(RiWD?Z z@tRE(w1qxNZ(nqf5oQz=R%irYWWAk}b#C)D-0IgX`pDZ9WiyW6OLyFh^#VI~REE;?? zv8lBwf!DE2#1ug{*;!f@X2G4^ruJP}Z{$y-%I2$MhIZIIK(kTa_c?hEZi2kvau>~+ z?RVFgKi>^@-`dcSx+|%gxblPo+@1#EfsL^KavRIbdVeZ&scMzAA`Y@-3ErX1H{_=@-O}{z`l_q706{Bo>>7gH)9~o zGKzThRV2dM<*=qO)MotU0I7bj>2)fT5K$62oiGpP#-o)o?oNN?1YjR&_eI>bH)m?u ziDA1=sTqKiM;tx$r~`i4xcMzMF;gbsd~YK@_PNy2d1s&oXbn!28`LF)vgK2*WY4Oh z;e12GhwVFoDarEwG5bB_%CTc&T6bXi-Oij4XJnr~B}7-Nl3icg(svznnKAkp_*$M9 zK<%=U^clbQHt_f(%`Y9EvIP}3fT<7o9&`P(QxQDkV!$}HU#p(@rw88;)zS3#c~>YF zyZ2XJCw$>a#H8iOEH`6h=Fo0Gu{65mmt~G*VG2GQ=~_C~7tuh6>DG2xSrj*PMntOw zK-u6LZI+l0L8#8Xq9?S+`~v+vXr4A5{8-a}g-*&BxTWFN7J z6fW_Jb!Tx6VN9Q92e!k*EVy&qh&w$`U4vf5yx>ql3Bv_L)}pCD*D-2AhtqnT$pu3k zgGAk-`8swvz13tt7qv}*jPF}CfB~@2#LrO1)DV~(R_65Mt`($XMaFsaEU7=e14^gw zLi9=TO!SwRAX=L#L%Rfz!-#iv0vm zY{3U1A1>ofeA^5fb^iu~F#4+d&U?fNEwZz9tUwDI_dNG(03N=P$AF5=ago~{#O+geyrWn za$4p{#-{vU8B5|1N)eUDBpY||dwHaU_+`Kq?!$P`ZeT2=|6G8@0-#=kL;Ru*soTYZ zFR=*h1NCUi?j%>t(M>DrsqH?)4cIZZ=okVp6hf3|TIev?si>}ux-Jac4(M=2!4iGfn3yyY8N?zA# zl#ubS#=p^9B-E-50H{9LRfGGp-0N|Px}?r0_>S^lVk`7%T( z8FH!BlNh58T!lR^1^#3QC2K5yfpwj~Pr9V7pTZ!W!k74pCLkgQk4NoKVLeW07&Ho_ zR-jld)ciZqJX*S}{e#(!`9H}Wq5z)-b@>i({8+@0^PtvHdJ|um|Ww8~1)HBB*e5Rslq@G?(pmWbhYBZ7Kpx z#P&o+ir&&BQ<>hB=nFwOSy|E1iqNNqbKjPS&PL1eMrW{YDLFK_ZnegTX`0gle%_{Q z?%AJcqKU&zJq>mm#@^x)+9&txyt(x)^AnISr?nISupDIX8-3!@Fgobyje|)XY-^f0^JcE)Kn+%mbaWF>LD#zRXE7)AFWP+(TQ{Vnv9~735=+PKE?_tbDVVNRn1(flr1_WVgE$cpEo8Ps=(TM_y zVf%+jsOvvx1+gM)@L(_!%q@Qot)#UMqDp*8(`S7W5>ZKefAqMVy!;%RQwZ1U3P zlPl7j3tnlKIw}J|Eq2R`vn=9izvlCzQ#Tj@<|Z>5O*RV#b13}yR_Lx&eNRS)c zN1GMg@Y)D)brW(|a5}&dt5O$ul5dlS!x;q8{*-V}AcdvGjoRcqUQsj+jd6rx;-kCR zDH%mM`1*9||G`W&NNU`YSndFEn8dUiUb~$qr`kYP3lN-J?6Lo5oTN?Kp^vm=wL`%b zZYG2pY&yoPEd7WsD!DCTsh%oqX4vAXu-pKdkfEgYwqIOUe!tVsWA?xBiD@E z{>jj;e*-~=`{(V;yMgSX&bvSCSB;jNKrZ*|St7SEfGkc*CIX-Aue~M}HiEdpmi>DY z(U@L;cLGeP5xkvPZz?fqK52MZboiM&E?YSjVQ?yO=Uof|PO>^OQyW#m5ji~yLS#B@ zkuoMHlYqru!tnaIDJAFqHzfw4gVQ?ZP%Hc=?#c ztV{ul%`-%SJ+<|edKVRM`sUU&HfZYVswb2s{Na2R6e;ehKf`AiXGsLnIKrT)4+eB7 zPQG*yMDN_oMy5l*erYCv55A}~)8S}zI%S1vD-)sTvn?V$em%VmUvz67u;bLo<5%2-1$Cy0mq7}B4U%Mhy-D!Hmeg7X_p|BAVkxLAz>KD@R&L9PPfX$4Oq?2 zNTDbp%Grpdo(!vPGijOl{g)mhq1gmAKA?-2@lu|w!VD0{0Okz$GM%p)V$Lkt;_(78 zeWrz~2jib0O4BVGKZw6T>aE@ z4?PrbMOfWmd09*OKnh2)be)6U zWdv6~L$XbnR3zN5HRkM6M7XOHk$B@PA&Y61)O^n56(s;BG288mKxS3+8)j}&U{_7g zdr2%UoH`yEsH=MfgXIXq59!rsR!2C&-ksECD%%~U6D=v}({>v~E=Oy`xC>{%Jh(|+ zG#aw(rgq`Su@KS)rA@W|liVH(aGGl#w98dSAFh84=`M}qtSQ=n&}K$L8ICX`)orhV z3&dKU4|S)cK0oJA{Iln$1`UmD?<3ehTTswOnQlGq^gg|g}~0;2_nw*;BH2-+Nu`SoO%?g4|qm$q_7tNSVqCB+bBtssgpIxXb4Q1&7Y(A zlQu@N3@l=jLoD4ILVDGc$8=O0Q)NHysoodSL0I2*XGsnY_}BO6@_}k2qj~SU$}FtW zXLOVZ2UsrQk@XGJBFn)eUaBBzq;FVk=<=-bpFi9GhJuo!uaRsK!h4Jw?+`B z;YW)*7(78qf2+}I_3Ha4xkCgAWUiSHD4auF)>-E$lmXFP*Q$qc7|YqvCpj3I^7mAZ zufLXAOBLP`)CTcoIWuHi|BSoHWFAz3DueRxN6848?Ws4R;P)#+2yXVl>ilnhNu4TZyY!50V0=@?%U+P^xHoCsLukjU zkv|IdOQn^j;l4KzmxoZ#EPDOU%scRR0mf=EX-Uxb-j%nV9nUi9xWLs=HqObOSN#`Y z@i&H2K~VV;N)=oC%Ka#Xc@uZ5F$x4hXqjtHEnpb=$!fifwSsZtlbe#Y~eV! z=4<-=7@!IrPxvLU8;xKtLr+fRnZ0wNw%DTzkZ{=dGML3$ z0IkO6sPM8-y4!u-g^K^5vT`s?m1JB~9l)OmG3wn}q1Xp`m_7&s_%B*^I zTqowW!#Zw)w?ouLXocr-ai?-}p}VJ`EIePp4#N-~tKB_IXhkG0^^UBnwkA0!RSh!Y zLm?N0>)oTK+g&3bcc2C^`=Enm`_%VTr}nL5B(FJX6QoJ7SM#nfp(1TLhpb`&)iG?5g0{3yl&Ng1XQ#&!xi7yX6&)?qN( zrXHOdCaKU`+C;PyX&1j{XXAV!{WthMhUo9n_e6;w*{&laz!fazquKhJL8nLp8LWZ9QgLN?jGre6mCuHX$r9GJ zttX-zX}j&g$s6Ewj@l6tm0j3drZa|ML9JZS8_R4OY_LZ~wd0mv0`?)Nn_DC(x^gI%p8W2BH4KhY+j0&9mUxz;j z&l5;H;E=hD>?oUkOMFg(a6na;2=dk~W~IcsZqaGFcvjNn3OczT=1+tM^zhs+BRo2} zyWBsx;?%j#(-5;)%$hEOY=x?J0z6v0EA|$N*#N`lyU2NOzH~QY(Vmfq%xFtsaFoiD zQGr=|?Tv;bG@~p5GLVumF1{!cRxXaN`FzNs2ebjCBk*I>QUd7u#m{?(HJ&cvhWGJS zbCKOJ#Gd>dg3HoE{Ypenu|C#BGoU}`p@;>@N+!xj4faO zm&7Jj^iglKO=uRA45*fF{M|8ASWHkNjfhLJM>Buy$Ydsdr<%;^xKbY1lx9*^2!`aP zwuOOo&z+S2xFqr4P=eEE%lWmb^CIeki5ZW!-$^CLU)rs<9VyR?uX|a6h>n_+48H82 z+zk*2R&lw7G#Iw9v?UUxtyn6a4|?l zQJ06wt6N<;(!zLpew4n5&m>zFT$IjtCaojE2Oc>i`07kjZJd_w(uz2S6Cq7@Q@_)g zbbORzo^!ZxoLuoN=WzCMv$1XFLjnLx03Fp{+N55Of;5EM30g?zXbPN~@h(0%aNY{v zCv00t`yVTGm9=AkqV6Yf^fyKOb5vG2 zf;xRF$6_kq?U2aOpKW4cxkKn1t~#F56;%RkP7iE@sD+?@K2U1wPEFi#L?UsQ4Cz4V&Hq zQXne7(B0|Udo)s%SlfS=(+dD`U+&okDk;TJ)>-&4<}Ao^65=HVo^3w~=}>~AD}k$s8Ogd5c9J1fhhrs4s z??~qm^YtgW%Tc#8_TDcbm=>tUr8M05*tXkyH3xXpXr^{kMvw{Cw84o0z-LX~K5 z;nNuR{Uy%FCp(k3gkLDgL`e=jCDyN7v#{}x3-hB>_WSG`Nc=$Un@@61Xw^jwK!RR2 zU^jwMFa`vxU;7P}&0CwTA2}45ieLr(B#`7_Pah++7}x|?KmYE%e3+@C`G#w_m9(KX z^no9mq-HXu5XUh@%@zNj)!k;6YDP_wL58I`p%dq-pVbFN~5r>9ay7F3^oMfa} z@);T3tO}feGhuUqPtLpUaF(VyGNxBk_Cflq<_bQ!#2ftYJo8O&)Kt$U0z_#&yLE837W!k)ZyK$9YU*- z%W0M0s9^P5L>H|Ekh{o%V#;D}iJXtcw#uHcX8X&`8kzC0jd9Ok@b@!7@Tparf@SU0 z1RO_7-G7o=#->@)J^o+IedV65A5;wB*suIeU|xoNHphPeS@7B=s;Xy8yI(bE85(Um zN<7DZU|y3Z?1Q76V>EhL*ZztUqkWF{P|(QvqC4VSrxRZq?Je#qEiYGeN#j7jN+O;k zu91GyDaRehB4g3Cx!--m)IVV5H~wg8n!56J{~g}|<4a{s5;wk9pNa1pr9oiK0&KYI zhp?#@tmH+(46#e<25ltjvOYQb@83Zh#&h0v`5A8NbAD3egcwESY5vFBCcf=aMzWYe zv`RA9Vp3&gvGRc#F5`&+8UxYOMGp^*L~nANf3{5!Bw_L`@l(QK-6ef;6YxHmymc9evMJ?_Gw&`A-RnsY8NI9W=eJmcvda z))1JcrZ=x|e0UEH3ylWj`c+^m!7e(TQ^1C;>qG!h5wHkZBagHI2EpT5g~Gb`2T?A8x2g-L;$K-wtDpt4KzjU+}<_~V&$HfL#{g!5m>KGe7 zr_tLjV^4x>4*ev=vi-?kPl99Mq$M=`OTOH6`@V*c%fqCG-O{?B`|G%q!A_dvfu{&)lS8rNB{ty9KmX-CZe5$TAY=-;m)|Cx1^$``EU&-UWO#(`=PDp)oc&`@+I63sd+u>PZMPe;#e!|@H2k`jcT!PKVX$3* zKJrTt?N8NYa529D6*7@h^Q-IutaqV`fjd*hg)aUi;}2UkU6ie>76RSKV+CD}()3RP*F zx;0Y*r}H(0BpGsW5NAw)xEr^6{h+}aw>9XHfUXVVl_+i*Ifl`ZRng2AT|lzcUw)s# z5`DdLt{lbkibT~D7R?+@pnF$Xu=;s11BuAji~*Mj9GLS~<#lQ+;pQuH(}bMfMg1D%ALCAgNHlJDVk zWnwg12&HD?8|f=Y6k`^@gawsq66?-v9ozvhkO7IaSh5VaY(weEuD&SQFs zJnxTavNgTL3g!QH_0NLLkHi*Kt#wGn4=N7{=-W_)q;#ms-5>Mdk2Enemnpu*S@QMD z4Lot*;_MPa%)u32wf4U=%*ef-7Rr#~XUCAE005Ig0h@6qX3W47UfY|BpjWvye7zy& z-to}u7S@-myYJy_J^3$sSe!caN{SL(390V7?%>be8=r{)P?E?vm)LyJle?$%1|B>= zl72}1q)2omE)9|;@q5X&A{wkW_Ap5O%Q3RHUNu>&w$ZLFGzm*=bJD>h&hDu6^@jCJ zF*0Gdgc50dmZj*w$)$C4FFRITjV>dC{E6(veXZzqs4b4OqUhWb*H)c7>;8Zn;cekIfPbymqcW ztt~anCB2oI+r>@4N+WbwEkK{6&uV^Y6xX~cPwIQt>?4d2zI7Gms{SkyJWdaa9Lc|t zfRS3xv2p|{Wfgfr^=Eeb^rMV-^eY4yENU5ttGkkAOKcjwRa4b8>AzT( zdhIhW{riJdG!YQ4ckJu;4bRnb^dXlwlV5KMzm12V1+j6$T=qwTbb(rehPIDt=CEvA zW};UGl)(jAM|4@RKWMe&Mrb3lkk7l5TWB0N7)F#TYp3-qNbXz#PRb2W%kmGLUxtQ_)W^oW=_%o`M~$VFTLS+ zs$h)V=Re7Ka8uKUsp?W6$s6aIBl^SpA&?U_M6p$5r8Hy;Oq+S^M8Y9T+SQkh6s1JA zaD#Q5U_y&oh>PvNUwz72g;_p$GFm}62(XH37}-il0WyKJJVR8z%oZKB8W%(wxpTqd z_cuw!0v#WHMi5?ly$C|=78<~y*DBS%SX(-tmshi?FVW}wB3gu`=E;gkc~7+y3V@Z# zUHo(L=Bb-XDBp_UtNYq=OqYn+{tlD4(#y}@yCwqhZs{gquiGbi)06Uo7||r-R*&&D zZj$MIs1*7NClpyA+hoS7U?qX5piVc}%4LpyL>ZytlPdL*@ozd&NV_KYw6r;rM>cp} zQ3FQy`@54K3oc^Y9gQ71rLphZaXAK*YCVuwlh0Uaok^@k)s}cLdXju#u9VoLt}fv+ z_f??U!)EQ4?E`D7$D=cGxU_vG+%3_^?*$1MN{9A4|EFj&YLt zQsHD6#p~3senXlh|A2<#c92?bKv|d*rt%44kOkFF%RmB_n5`|#Hq$h^h`?J5Gx36e z0pHTyx4BN39ZWLq>0b*o^%FI%YycrT9acdm=v#7uOI)9LptF{uIGy=;cLjMA*TAE4 zt%KR35Fbpe=LdG{66$e{<;2j-xsQ?jFz|eDJDdJsvI1@=O;}v#z726$Xi|+pY7n|> z8TX6ZIlhFd{1>Y%_6!2uHYbPU=*VLv?in<=QrI=QQ$!+C2I4zbs|31PL2m1=AD>NwWj z<2s-ouaghfbxIGtARjsI7f7?TBw8JagPoGWBin*Mzh1P75qs5W`-F1Y-i>8mqcD(D z!NZtYsTr&KP3!+rE>T#McMw;TWxGQU#)4M3C(S54q$Hhxz7AUfMe!|kk`a#Buu^;Gtjkp!yo(hP)pC%&r}Pv*@i^G`^G zi$NY{_{We*T6uu1wHm@An)fnlbWIHwH$${PYj5ow?Y`lA?YBv%oz36Qo5&)!>Bd*s z-%jfH%3@2jLbdGZ$&|ze*b$YV3ow) zatXy}uy4aF|C8K7kol01w(KDH6#KU^J{Sum1-e@!2^9Ud@hsa&)99$p^RIHX{`Aas z<1*@@x>!2B7#U>qg)rm*+0|49fUk3HNu8rR>76D7yoPMOo#u&&m(_om(&6 zgSv1%wH!f70f*TO%U^27LWSX_>#I(+np*4!1rA?o@@_Xa^gJPt9}DS6)o7|HhDK!} zfvb^-&rb1>2nRbECN=wGDsd$iO1N#uP_~(V!m5Y$!sjS%0N1~2Vf1NBXglLz;%hqV zjySk{?HPZiO0Hx+C`uZA$VV;%?wP-nV;)Y@UeNX9_mJU1SC7Th&to3x#ZqKXy0{z+~>45-O9r`k`7!y-BM8HFlRo{wF+B(_EO1hw*xJZc9cveRX>Si?6R zqY5nIN@k~>gjMg!cDjDChXiQXGE@2Rwit(80st^T@tj?*U|PC}2yWfwtP}y z5X}`c$)A*rb1!CW2dAfjELX!r%?i*Lld7JGz)6+%7;1Yc3jNCkE!0zgFT1gWpexvm z+p21_&MA`3aZK)%o=uL`Ec3CR5YQORX$)&DJBq;kNwiv_QFvH7?D?$lU?|HoO}n49 zRtVtc%;pM!o;_O&E^!AgQ4)Zm6h`;%_#5R+UUrABuRBU&O=kMKmEQ{He)0p7rInr} z4*P83Q7wV`KgnGH#JAOFf721)bJ1oopiq?|TVd--!V>YW5S6c+##exp$imAo@1$(( zFO4Y3Ekw4d8wXJma|KC9D!0q&POyRia9yx0jEdT@1%#z|ViuXS(d$k7PYvM8J9gJ| zKbEo_4%^A?PNC77!cv&z^@1SVaM*+JXujh_Dl1F_Q4nQNb_1adk;G3Qw#*$Vz3=I( zbat}upPb+9NR2KSEOzH~N@U_n$10^|nAO8Rt! zKgD-PO#8Zjk~<6nwh2KVtPU5v;Z>D5Gzn3>ONxUiy7djHwk71Ti4}+EyprF50NJI0 z>PqK?cb%C0_abnf8f?=3ciIzsv1!iYKZ0ymKUnrlmNV!h(y`5+_c6c1OjeVB7nJQ(r4 z1}j5VXwW~&L-yk6V83oHwbEGeO5;+dxt;SudSAHzA2hVtTu6jZa0L;Kp0HBeBUB6Org-DoM zQ0h)LG{V!^xb6B0DKK5))UlQ&1trWGXn-J4rpHa44)<2?`JQxUyMaA?$= z4}Jzve(xK!<5K=OT3k}6orR!(fkgyLM!xg0tK|r7sK{)N3Ozq1mkbt^HMlg{Etp&dr{*N`7XCGZ*|vZdyVO0?<33_%G&yqzau zh-hFH;CTBrECF1I!_l+Z4tv{F4~T7bKHR%D{S5ur-%TD#QRT9>xp>3orDTCVza--t`SQh zUbI3gy3dZ5xH$M6<^X7DC1I|jBaMCC%Pn$dRyWhx#axpavb(h~7USG*D;ocZDD_0Y_^|t9nED$U$RG;6gnpC@(_f z>0ZJF8U5$QvcDas>_CkFSa!FMno9uyCPrD6VL`{*@aS1Ui-Wa%5N(fH*;XR2?yrV6 z67vu*(8%F6cwQ6D=I;IPDa#MG=TVhQ+rH2lYM1~b9YIa>*%)y*vs;XoflHhX1Wm!N z(W_Xci_QJKf0BbVNu0RmM+W`QIIKbY1eC)_YzvV8E~2($B3oOw_!(ngqK5sRfO&Sl zfvU30Y$>IiPE()U57`kK3%Yv`{OEb@^m!V4?3bCQ#?+XACuq$856okE~FtGR{tGD#`f491D{*ss-5JPqoczsPc` zuWqO#(x(1d`zJYw8|w@AG+(e3?I+1BeFUn3sMj^wK}cWj2a!#rX}n7*uZ^>JnXn|t z4umPqp_D))g@R~nJ?m&Dcw8uqgM?lwDDC|qZ%x9GXHq=H8ZB+9e_=b5tSpNxh39~J z{;TzElcF>>u61uuaZ?&7M>UnV3;xYBms3K;E2`M%zawp8u#^AwuNHzBj+Q>`JDd+G zGygHqn9{DG$-Q@6lRF@M0BhJT5%Pbemr2S2Mg7^8mCx84bizdg;4@1ZXmy}8mD#{^ zK$yK4X*elTg$%MN>qC=(6kT&A5=HBIHAqS0fEs8-Gn>Byr}k+0bVq*m+*HGva_!QnR8l@FZi@!`Xo4X_@lA#8iH7(*$9z8L#_&`Xu;xXQ*g! z__uD&(U;=)e+L&NejzjkIu^SUEME0Sp=ye*Sx}>nYkM5cvNOcU<3HPf@JTKT>8gi= zQ+^n%Z9LpaA*^o4a!*|n8Fy>xD>MI-DEuSROO-#7yrQTc?UREZocU*p5t}sG6^vlD z--=Up8FtdgIq{N2#_6(Fl%|m;*(|_GQ|aUY_Xv~=mqfG7nd-&1w%6Q=SZ#jG{Ub!` zjY3*EnX_7noW?hC8=_^WJ5wp@MW4>BPEtcH14PMs@mib@KmnLEM$k$(GWGLZu}}?v z5X8C^e9eCINcCvUQ9DAf!6oLP`QsK95)KR@dsuMlp$JZWA+f~a1ZC<-6v=HlD`#f{ zX1@o*$pWOExb*OE6y|TbFKi`YTI!x#lQpfVlkK>+oI#nb#k#nLryk!4Tj8NovgoTK zVo=>cpOpUH)$Jf43j`$JFJ;IDmxToB1S97P$PY3Fc#~uu?XhLYCtzIbomADWL{4z( zxB5^VEh^OI795YkNEQh?)H_D$xu+(7a|A<<-pCxUpVw{y;R8!mZn~Gru!lfRs=RH+Z(|>wD+B=r02& z=xX2lji&a;(9st$!5(nL{{2XIP-Guw7mcR|lf(MMPqD^ryUc!D-bARP42KqB4%co` zS>9IB(A@WblG_Lpci~pc92B_crUgBq&2l}kdpOQ^nTSeRr6gp1-W zG2uiRN=)m9NDgd4Rj}@&!wF19K(-hFS2#t`aSCn>x>-EUuP!cawn+*Wv!O5e^#m27 zoC>ikFy&)bCL(^_?9?#Wv|HE$AC9+SUe7T{s}tIPbDS2B-uO}weq2~FG3UJwqcl3@ zoT$2*B?zn4JYO>*Fd&mB>p~;282^y;&JI$e!;9T~nE1@|AYIwQW3@gx<-s=iNx1Qv zNqm1G6#$b7hC-?&Fk|kCMGrQVKq4O|GH=ve9k_LiXdujnGvi&2+h==qX*!?E^E@N$ z_4EFkL8V65V1uw!sl3V!$8=kgW!G0Mrd}=ZTbbb&9~zxO_OCSF3}RL$vd@af`E7e! z1UFgg3etbwCCFynFEw?xF|xc*(j6d(&&UYfoU8;a=G7QcIZRLjLC$t|E+uJsOOui{ zwTFA6mn0nVAg)7iRrdycQOCWG47Xw4nB?bHb4EttSPujmr63aKiLCl?WGQHx+bkxf zP&!(Yro8;503&xE-PY%p*V;eq7yT2xC%&yRDzh#NWT*caeO1M#2@PEzP?I^akzjXr zekgA2?wB1ST<9{e9m;#Xp>Faspmb&oO}Gx>5^IX%8Z-%fy;)w1;e}(a9xmk>rlc%d zDIX{?lyi?3=G8}sft{f+tV8)5x2+{5DPA#Nah30BXz_IwN2A^27vsIn$)OAtd|JFs zCFMvs9HRBmLHwX3!LHNsh5&6ft&&yQh{&W$fHXLblspr67zs6$k@l0XBzHQTRT(Ws zz_dd$B|Q5-$z1}(PgEduIiUej+R6tM$~L5HRxL?XmB*PZTNJh|mvABv^e@~i!_;*B zTNrvy?5)S05W(OWnyY1e3+C&o*MjMT`2(cqk>zZ8G) ztXSTkEtx#x%}kq5t@^u?W%R|wliq1;N9U`h;n>tKs^kahjbwIFGtPjSQJwn!q!4{t z+TRnxUt8sekbv7i-ji5QWE)^ZNECqlSgcn0KhD7A^E`s$uA*`2r9GiM-F+mW*T(gw zDp=D3L>hYX3SXKK>JdDF#@j+4M=L*i-6+5M>uK`hJLWQ2X87Ov`l7qhzMNK}WoD}> zZQpUK0{)}R?ZfQ2a8DB+)4Q-qM%5Ei@uL{Y$PbbScpVUx%UVVi7)e-CP|qxNAJr4N zW$a+vOs4wxD~Lkt?{KC#+Xpo?njV?8TtrIZBrCWDf!?rK4y-*l+D1ado7n1?voI|C zvBmbebFpHO8U#xH#9OGE=c<>-r;*q@*IW63Cq&x=XNl2WD69l2u)iRUrsR}VJc`KO zxd8@BJGrr?UJ$Vk7d|=X5|tGzr*8fi^@+)%T>tO)5lCqpLe=<4WS?#{=){jdLy?7n z0xR29E$-hgY4m4?SN>|2OER{M_d%W}h<$*OUoZ9JrD@nnm7HuNN6eV2%8L6`S03)5 zcPn-t%5n2S9%%)O;{hf}5CkP{a~4^Xj_aI);^Ow&5L@3i@#eg@wksjR)4 z^%-h7*2Dv8ed}sV8b>`vm#Medxm-lIqCS}8Pi8VPG{NITD{>|CqBteSe8Z(j#t|9PBp}U;FeMjWL%la z6bW)GZ)9gbSlx7Qj-E!G`i7aE*ZJP-w_f5c`lP|f~xdlc@w zp7+-Sn`IlFw?MLvl806qXZ-abK&ZNtZka}Cti&voi?y%-l8lO^Fe%n){H)=EvW2-? zIC;{^zr?x)BOb-A)=s7Pg{`cLh=3{{fonlg0;T$DDCHyM6C%^;vFE)onQ(W*z58!P z4~s%a?o0e(SX)FID|7hE%iLf32juXbWfU zO>}o%3|(!cR2055M(|SKN1-!UTggEEYQIUPU;8OHsOEjr=mC_Im!ugj^oV5|j_De~ zQKsZ&KZRIWXD_%BVFJ2fG4Kc_BV!k9ew%|LvA;DI0yvRz=CinhIuu4zyGA<$jVo9U zDuyV~rn@_9wVUhO#5PQUMg>}8gv4l2L5tOG7ZSKrdUESn^IbG3*)fo48~I^{a8v}J zapTnJ=f5a-6*TZ7@W1NLDk|!zZTo+Qp+jQma7gKHlu2;xaW8D==H(S>-shO(vN?Ftxbg&D?L zc0HZ{EVQNGDn^4l$F5J01IiK1BeyJhNjC%Kjg24{T->mXv*RCE0!2VtfdliBgDS38 z%5wPpT1Fa#XLCY!H`>%XMXR*0#f^pY6)#mbvj%Uqf{xrR3nP!=H+@~57tlLeGw$+y zJT3u!IZ(w6?&Z#_MP@gt z%&_eRVh`Bbm>N1SeEy&&BsOurRvW(9z3jYjSZ+L{gtuZgH48k$p=7qyn^#wI?;Nho zO5=kH>B`y`hher_wfwQ=RdHn`xWYiA`hP4J0CX#;KhgWD68UiYoYC3`>;0_68wxC` zDax{WXrzS4@W$d>TcWl+#nJs|bSP84HPtr0*TU*{H}on7Ta6b~N)%)m53gn^!5~Or zL9$b>-J-e}ZK)-vz?ha$BQ z9Q0$~ql4?gVDe?^+)1lplym}15TS~0(?7`}LxJ|C=k^(1m>_GpZ;ZXPDs3bxeKA_d zk_7pTHD!^@_u8vt4lyol5u#~~MR4+1*^gLBLHUHzagw)Gs9rn7gqI;e?8F==1)%R? z-*fkm=<&eONk1y_Q-oq`+vqy92sK6)u|2k&y$^WyS?BA~*yPjGw1bZ(edQ=5&4C+% zdgkYmn-k*=0hU?McW5 z(K=>DS2o+HU;WDTOI%2st73azNfafRx}nuHF}J`W1pv=n%3dzgSm#uS;Ma@9O!q@b zZQe{W_>|qXSxY#-m0LCwY_+|5!C}JRW%j*a&OBApbS%I{ijC**=~Jt036CyQLENC1 zc!wc;2{aYz4MG-G>KfsRdnXJTgUNlse;As(aG7;Moz`AifAIli6as?*mie>{k_!v# zrz|z$i6WQ&j%mwR##hciG&gc3pTLZY!zMAVnIyAoLS|4;-P#xqhN3Y6`97E<4Q~BG zxIIG~QH~Z(A*=polIMSWXtUDKpRsc*@EQ7sdVO8Jb3kKUX(l(Pr|VIq#2i532*wG@ z`FO(3A%Km|6Ty*aH=uOisq;Qn`}YpDr6Do|RFTZd*lG=*y_qBMnFCiRQgRuKJXhXmmOBW z`(|B)jR7pL`7r~vah)_(BD41=CHg+|l@^@?dx}lXHG&_|>&g$0qb-Et+vYkO>U{iJ zGtiW|`gQd*XHhORryZyxQ~I7V-&RQfAIa^3L4z9FgbdF=2qOa?8R=uOq<*RlMwgs5 zXV_)&#I6}zj+#9X-P9x64BjaBg#`cM8tZ$h-g2YpTs*MwK~4Pg!!W&Pvqt0ZW#=2w z7kA(i7t~UD(%8UN=stRk*TB>4zwHyNr)Dj}m(9+TPLorP*@|>fVvhJhjt1v6u8D4C{R;aE0I0yC*DQaW_3vd@31l>Vf% zM5S|Z;iz3te5pWJU;KW$6O#!mT_m<{>fgqCg4wH&OgU#Zy5>WnBxVXD!eM#}8;p5( zo0ws*78mjE%aIt#ZbZPRp`RSTK|1pXG?3n(Zb}^7eRrw@=sTPDY2~ zDIT5O3gT+cb5Ynh#>31nU@|mjd0MuGT^B-r5h7lfONYY7z5#b;`Zos84A>J3>!!f| zR;V29X?4mC(ub?K5lutA&tS@ppz&~;J$G(Dn@)0U;Oqx z2W`&HuhwTJvOcGazJ?&n-rriomQ&Soi(da$xkETAs^p?QGuxxUPB{aQg#pgrSwqDb ztlKt9M@F6q3ESDF)LBws4esSw<>$9Ff>27H9Z#K+8`{MPJ-@l`9CZ88`!FpE+6>*K zzx>fvOarw-lID13T!Th2+gnTZKZ$HE3R}0R)GvPn@&8@NoapHd*y$Mve(|9PqV+~u z+6?ixtQ-oczDugCU)eG|*H~qvGf_4T(EGDsW1kdEpiAdgHTj0eEPko-aJwM9E{U0* z+d+I#)L#9&^4PaukyAKC7EXwKP7=(#?*5sp`|NPdhnG~$88QhXUDa?@0wrlq2)Rmo zTd+dr7fQ3r-PMB+jBJWx40ocFs8|RDCt@3VYngwBi>2J=Nv%Xd>QhO#`8$utOF@oJ z3DX7)Ngt+>`UWL zJ$etSj%B=RN23N6ks2;NItKzM5gPm&vlx}v0wcsBKNPhkDx4ywm0$mc@f2vUfVu}&XN3?iVWJ_J zKI9nTg#I#2Iaw#JnrVD@mQ%RHRbd#8IZJOYx)9k5-x`4a--~wWaWWR`JVLFQ1+K85 z(9{r@^#W>xd&0eD2aqsaxI-s2od6e~0>| zZ}I|CD1fod2?3bP)I73`f6wYBd8x%5MT#$f)p*$vas5^GlVdoKQBVIz`HX#xRKA&q z)bcO_5uDa#9HT93_|)H?DhK_ zb#|}($R`A%KYa^LN4!gvGx$JgxvpSZ{XaYzs=nxLcjN#dGmnxb0Fh&LKH|-man_qC zfxdn%&+%tpq{Rk-7~?pV{M$rss2Ff11u%(z{77uEC>BL2gST!k+RQRuuoyl6>El$C zAqrX`4&&|uAO=&>_K}#fr8)U228TfYNe=0@305^~W1@Cv1F@@MGQ>sG8QLpAtJ21C zsv4nK*tYgWk6oEAg#GBW!*!Cith4*`R2V@~Bozji_@C&v?=#-3g!0XDg7}fgLrgb3 z4x76J>QB<=a~ruAZJeJ2&0hRz4mEJAqCz~%D+VAjdhD0?0X1(Up}{$qJ3qq!@Jy%K z$@8>8$Y_Uw1T~`OP5CRIbUuBp=xCYvZHJUK3S<3`S)v?IjJme%T*_y^o+7k#0?`TS zg4yE{7gQTzm>=YIc!$wE3nMmOg_3?*@nnsItAGJai?>8M@gj7sm{1m}bGYa+szQ*i znuJm>CgLN2!cp_ca(eYdRvWIH{n5S}#Q@pm_*R_YyWn#wQ3cZhKJ=!lpNnF4dkZ&4 zpjwN#6xWllRli>6wlr_Kx0xLC#T)*U+-3+UkkA7o*29-(`~ zvPB<}7fG(%hueF&YJeI{rJ(N8iWm}oaNHi)Lr9@<+JP9#rwEd1k|i(UYdF+%4GcBS zQc{!e)5)EN6t_67Ua+Y`WcWV2#b9)!KPgbG{gYd2LqNd_04n0WHm`U^-r*Mhu<4Tv zA(BvYAqr}1lIXfYJUEPBz4UT^-v59=Anxp%)lTcQLM*G`0)UxwF|){Ec*$+n3F1V; z3KqDQyIndns;Rd2HsC%Xj4i+8KNZO?04m^HXiI|>@>&=>p)tfoHV#`Wm}|!kU~@DQ zb{-&(k{L?-_oDgg16Y`sL&fL??{2OiJJOBPXJomLr9bw)rj z6zm;}qG0ok{3ylsTkhhonL z#KT;TnQ3nl&FSuyL@}@L)7!($-ACg(%bc2qoB*@cyUK%Ad=RG4RVD4~xrFx@djLSJ z6jcifU87M7Tr=;j(Rh9@)%Ot}mZ?-nMj%HCr;g*Jlj@m^fbfDs2I1W(*UUi! zSid7VWizbIxZ;Y#i+6|Eh7~X-niD$PYm}MO$;`Y`6O=Fqh`X7L(d_hcV~&ks zM+rTmo)kd! zT14}!6bAsp8{(*t_u7&pL(xa%*v&O;&YP)ab}S;ocecuSv^}$*P5{ImiZ%C$e%Nl1 zYSl~V3n>(Q=fOhwQIZLxTqbAny;)8IQE{LNhP2;^ZR1AZjr$4`W_p)oK(y`rU6x~m zVchACZ;OyIbJ9le@t{i63b;-2CCi(F8aup%1$I)V$*li#MT5bvNEYN2MPSU*6UX(lBvz7fWMM^ zG{Ia003{QgS+3Pv)_;<#06<)9Ci=K*U;ztX%8gDSVu8U?mZ#pJ8|#f7DYHx^DaBmM zhl_le*dzt`GeV2Zj{|`azZ=&59Vk1FlLLX__z<1ESR4%sYbh^&r7o<0#c_RXxxR;& zRTzWM&Z^qCx7fA2yPB#antdm#CFN$C*;I7oSM(>~;ZozCH*UjVo6Wx%6pfeneF7V0 zSelX*(hhdo?bpR-8&;8?PzWu8v~D8$y5EJm2M4<4lDQd6BC;pgMRfmj<;^0)Xs!)ijTV^$(pP{%iQ^0<9~1%zPB+SOLe7-!81Y!m$Q0@zaOj zs#F5h9~uOJ96I5Dl4}d14^vx&jdLV%Z1{=+${Z1#lD2ZSu($?n4m$#7(56*63c;=W z1YG9}N8gB_Rm~d+(IlgB`d7twduPQ}Pv)CnEiEo+BRu&hd@h7`*Frs3{ z!h}$x0JNt*rk0y$IB`G=05W+HiNo5s&IGSrmtg3}wf6WfS{QnGGA*6N;xTjxyYtn_ zhgKUU14M-hubW6iKc1So#>-q#9qAqy1m%V+(O~2&&HDlY(fhK64IP`Ao_)rtvY)Aq zGw8)$5i_~hgaFm(RHiTf_!4_{HRi1vz01A4b0=FN&{ZQAol14`5)6@+k!O2J#U1&Fv27tIO95R}dV~(mrtn@lA_Yyz)AUrB0Na9 zK)ldaWj$eB7ivwv&8I9YZK z{W@jvnM*7r#{6xQ8j>&Ko~wfW2B%5maRB>iNW$t1?C}#fM#zx(qTgrhn=uerBR-4}OWq4wY2 zF~#PmoHq{%!UJmnr=?2NdtJu?i0Ym3BupMHZP~1by+9(QurNW=CGjHwP(S?NKdz#ql02$8nPY)uzPR(4Y&t;+rgg%CW2I* zn4Lh;m-9ydB)0^YITrqz9xTMj)f2@AFjGgfzIKv});H*guWp3KxDs;QCI7LDx9fF? zp?fueX8n7;Er@5JCm2o%p_O9fexLU8q{D0pxv4+0zQPxgmcwi1ls+SW$l*%ij$%4v zUsq?gWC%mPKVf>^o(^s|sPOzdJR_<~#9l7#G|PW2NkDyw0bWiz~n&E^*Q zb_gr$v)kj2Rjafgu6)Z&$7~c;ySJ-C5Jl3nXvHT}h!OzQsf>1*ncA47ak2(D;ke?Zj( zZNJVveNht}U|rmmVPhK&`jlJtIcY;V-7)qxbWMTE+QCCM`>}_m%22K_TjL;dG!r%( z!~LPk@1Nv;!ewAwrjsbN(E^3O>bO0H;cI!;vS1%z2+q|=h}Hh>&e%Ub7Hr1L_j#Jcv@%J?QzNZGbb?_muyDU1i*J}y%2kA;`4Er zpCufm;)SbXcub{y>1AszawC1S{XSuoD|CC%D(iLU)zP|AobK4T1cJb3$6uS?(7l5- z2FmyL2Xk`;5L-SDx+`$G$1xJ3-f3?Nv3fmw&Dn$uz=L^+#(Df|Z`<%1S{dh7} zPCLU+gc~ctuK%lsRK*-=3j&amQ$##A(8XANSEic41Gvh#RpPDV`4R+Ky|Jkeh65%A zgN3NPKO;tq-mmQ$P`g(4T_DLNaiHmrNb@9Jj@Qpu+TLDwj~%xm3`6N}t9~tp+T7_H z4Y=g#a@!cE4Yjx1iZZ&PN{x7%iif&tm>=xJ!aAO=R3;<7G-?OSyX2CE1Yd2CvHajm>>;4( zBCcD572kgLG)wiD9UB11V@Xw3unExLr)%IbKf~@5hJ@MZnw%FLsrz(Y5Y~7E(YNLT z?iG>`_z$(YmTZv|L{gxH%pjwC!OgGYzHOSP{?{WG{!~w8N?*UF9g@on#*}3JVMuz( z-tJ{nAAy;s^`wG@z?Akinefk9iCX#1PRXTf$kX%PHy%c&G>zR+{4_ORLj^l?LUAww z0;N{9;SL(|E~fEa-drsyjzYx`TkQQTd6B}ivwhapu7(CoU5L({P7~-vk4sRVa?1jj zN<=b{sBuCf<-pIeoW}@jzB98#s}HzJBjKs=s_w9ZF@P{B`ua6`*}N&UeCf~iUH?h$ z5DuDBo1Yk_PT?;zegV*nAVJTAz-6o|IufGPr8yF9cGP5q2f?`PNZbmGF zhF5Jagze)`8vim(y_4yd3xSw)<-l6Q(4kqB4WxhWD_`x$C;yxy6&1sL-P(z0_~|PM zXCWZehHoW}*MA7pd+CE30cM2tFAVpJbtLs@-awSb7X)Z$ZB+QfzF#q|eLGhJ1CaX#TqmQxM#swtr7em0G$M(hIha*J0iJ>^}BydqCci zRIB{d3gluI==TYRTGV69UA@}GZH*g8a+i3bh;q#C)1<~2U93VeTb_rZLf)c$cA?|) zl-QEsb2{?1OeAhx%jcln9QO&1FO8>T-u~;z!ruuTh_@tvoJAL0-4T(FO;H_v{GliJ zr9j}cx=0WDQR1#i?V?lCr@dYXasa1*gOs^22{5ldWy=sAe;>+u;9%+Bz;i#a5q|fD z-HnQEWv|sKvTIk4u(vy*<41c$?Se^B!^bc?=A>_=@;xN>*;?1Duf8WO1q+5qZ^m+I zxg(1?qKLCU3Y+qk{J|(%QdbgV-QSvSVSED9?b*TOn%>Tc%M-7Q4Ja-(Pf{tIvG5x2 zOMIQ>*}beagQw7jrAGdmgQONk4a_KXik8ByB0j=Mp?YGnx97upn{ZI5IGeTFKgsRG zLBoprlk7CHLZLp3m<)Z9BR6KU&^HrHBuAH8;pJ0HX8Zo$uBk_13*+2Oj4c-e9n3r4 zTOI||Z5w1HxA}qK;uCZ$zMH!C&WR-(LayV90 zlHlPhCjXpA-=~IQa2VY?96mP>FtUb-v(~7J!aR)4J@8oaz53w$)<^W#$@2}2743e< z&gmw=@2C4zq=ONWjc9UdG2H3A^F6LtAU4ewixz4<%g{EFrK;M(KBk?wMu%eioR9~Y zJt=%Qe7=g8!uE7*aw%O^Kn~gP+G;o-K^w@#=y_rwqM$Fg^{H)UxOgOjSyhnUzd`FP zC{lJO0CA}%T*txbE3HSFgvHY05>bMRDs8d+f`-NGwetrN-F14K%VWB)iPM7rB!?vM zCyE-tY^eTn1hbWZW#N%|nuZGGHffKHhj!4YDbf5RE8nT)PpgYwONWvf3v5+*b!mS$ z9p?LlJ@>31`SYUN5gHwSS~~jk4SK;}t-1j?0KwwIn{!~&E!_wAYhEOj(H zzEoUKJ7HH`miFaX`^)Ur{Vi8`2lVeiGD&om?P8O|oF~6omz9?j%W++cPJ3ZKCKFt_ z=mA;O35q!+I{dgU!)hLb=f$TTkI6>GzMMj7?db@P>D8T}O_y2E`bBGe&PoqOCp#Yu zR!(ne&@XH5e2eI~^x<=iTrPRhga#I3I_Nzw-1$fzMS@Q&%Q}c%;<`wdgEV;kbH<@k zOH7My3JLLp9U{TKL0H>bX3}X!al_cH2z1%mi(H9F#;b5oi1@^xkDE76a#+*m2;fkUUl90SPLZpf6equv-pr^K|*rzP@HW^vR8Q z6~$}TIqLS2r}QD_<*eo-jjX8=j-Pc-7cR@(&LMg;sJa}pmTa0mYyGrpej{fs?BT=D zt+;;G+~2%3_>Kw3MA7UfwtQW3WFP1^fF*v?-h90oO;sa*4q^x<4TQbL&Skza$TwGb z^{K0*8d8Y$a*1@GP>$4~FiX4T*o!ytXP9n2T17Ja6CU<)|A|^qC1>{=k$;lg3IwIY z=Co`eNgQ(R;ZU1299H3%=(tUW7$k@8JTa8XB_uZM%0GD$C2(vYqqSBWUl)dg&%iz=l9c8GAvZ zN&xEZ25n$&H~>Idc~#BFx5~l=Y;_D--W=cBb-1CTEX0hRPQ-;^gIEx-!NLgPj*EN3 zCe>hJpQ6faM2z{1uiV7^yGt_(t2lWkE$bFIs7};iee;$s=;`=vtGV{z6Q;(+FEZCk3ACN#82VzzhgII`Z;MwI5u{T~@S#h>p_%6RA!Ev`BA7Oa-C%Fv}s7&tV z#0WJhSN`1>TqX{zdE_dGTX#DczrV-xNlKJHx4_A1-OG6VqdN8MlfRkF4N1A&$OIN~ zG4@13`~;dHneD%H*?*1+)49~c0RV=gE@rKjba%=5jI6Rc14w*cEU)4dj8?Me5|8EL zq>`Y=PXo+TW1q_<)Wz1BSsc*$!XYYL=RbFFS4Ng`m!y7siXEsoXTVPLg^F>7`ePU3=FA5Ujspr;a!=%A ztfDWpi2Eg(KN$vr>)oTJ09E2*3Q^9|>wKo8VvN{}nkbb)rXv~wU>K=QXj+7z3D zdSb=pi?|GFk)~9JvbZnb4-=Hm3&di!xiDev&ROY-iRl>o|M@J)bzJk7OycX?fQq%j ziSTQDG@WPr3(JZD+JlIr5rG6Iv`QcZ8pcf@*CcuDpEAHiZqIlA$o2=L{TYWYz08{# zI2~7q%95ZD&&wwZ#_lUrV1Pb*#=;IPI;@>hnC0d5-DXA_t5D&aIX^L(%U*8{Uul0Zw7NX#M*<1o}LPM4Ctmcu!75?3)f(e<;z+#Kj#aBNa z%?+i%nMr8rou#{UBgTytSv|ty0nP`omx`BRHfN;1i97{fPiT^_+{;wW9-c(-i|;mKVNRs%&9uP~E|tkGcLy4vB)y7MO$e z3O(m6E_VX=1mTQKtII{tkxx=Oy6|LFhvWq*x~j}L5X;^a1SLN%F7)5OvPq7t9Hnph zdNj<1Hi8~Hg6w8#N{^ytK0JqE?*@iVFVs-!+hggvSG*O3FD}Y5+Dzrk_8v`aFd{3#~7?nStr}XpU z7*NMwOQ%?Q@-?t-qz{)rbE0pHFi0OiQ02@%@iz2$13OE4zZ(0s9P!@0X>ou5O%J~{ z{IEXQl;0dq6d4Hz+m`d&rNgI2g(`I-RcHiXsfwIwjk+2Z_4OsFMhvihBUi&|%ITL2 zN%8dl0s!U!cIsJqU~f=udao-`@|@@?l}t_;iBw%~kbwJgxHZfz3`zC=&&Jvdfg{ ztSPC5M$B4_u&me|6U9L~BY7=2xm z{)dKH^3T#hk;~v-=V;u;@T~f`47JBlc$GuI!(+Udu9-@-F6f1-7%Hbz`T14%gav@b z+6~Tj&M=3hKzN4qUaJhcoqgz+z{2IlkC{qV8>J1#W}p(q3$=-Ll0CYAY9W4VP+g<7H?1JVC-Qw0)~Z*c z(zWGjO3s@zMA>pvu<+o36nytMe?$lC$#TyFdKGU@Z*-+1XKfk9GM{@}6eEF%b_UCK z%yPuQ;|A+Qw3GKp%))ZR6T7{<4zpe_9UU5lUtYsD$0rb!5H)xC8fzsBY7AF}EH~8* zkH&6f35PWXSVt|ULE-Qi1=vXB+6o*tsLILHnO;g68sQyPrfli6mgcy^x3KlT=d(_zW0q*4FH2Y~_wk#kkRUl=_@ z7?-&`vh$}p1D>$Rj|>MZo=Axx_O~kTqtTn(dXc*GuSg47_6L4gJH5?RW}*f*XBhjE zby;#nZ%WZ{#Vq^t2JHn^_-Ib97O?za@qD3fFaP zr?qpx1+D+K2VmBSvoaL|Wtd?N^z640y&x&i#h6M(I#-OwS9#3nB!vTOS7wlT zaQ2GOQ|~?3(~`VK*I>RZa(`=?8e5Jw=ty&&VX}!hy16PDvbPmk%VEka2#lrgn3zlK zlp}pg6Ie+|=QNT&I{axDu!_EYK{nd-u>bp1lZi#gN_tL(<)U^oeH@d~1wB=D^RwjK z&ZkBp!n?BYpn)JLpMlQRVK)$&00)IUo=hfAOkXo^JV%FnN~sje($Qn2UwSlr2ZB2_qF{X69}F3U}dPWhHL+)BGa zen$p*o(KpcUoZ))^XGf0%r4aRcHLEz8s{T&H8{(l`G}SBZ-+zEG;!pK1(o@?at-g!Xo&Z{#07%EXHEKxeg0%9{t7bDdHJ!ED3XY zj@?bBafw4sBV~ehusCX@2>|Gw5lj={zkc(^^I3RS>?@(lRn)qh@F5+}r>%EScen5r zxoSreHbRM*M-0s^0~|oCw1kxM3`Gu@NxFtAc-_hP%!ldhy{aABoL&j94d2y{jET~U z2#kb>M-rkpr==5_`RpY&-PDxWkfy9u)!*w0llVO?-jqy`A{3Lp~ZVsUT(2*uEK}}T)>|b>axU_X1qUZvHHYd;sjCL3@l+w5BHn= z{8>TY*u$@rEl;@4sB~m9LU7@?*9x9xK5bFa$`8j+9a8b*T^_)?LSP_)lu)!U)3>B? z4+l6(w}$BUInGc}Sk;8~G<`=@#7hOCJF0}7hrQa%sVg8fcDj*{vFs;MCDgsm>(Nc`3zLG{YoqKU9gM!?ba1WMc>G={5RapjE|`? zkrxHC*}6Q$+<7@tK63Ougs^Q$+M(aeP@K{w^>ZwhP7iUx==o%UFAnX#_=2+gLRTgTDh9OTq3+ePd~OaFOp&wXoc^H zQDluAMKKfA7YJ#0(ZjiM#|gW)g>+Z~!wPER)>=Qm3$i$TmsW~DY&2Ir>Pw;H6~U5z#dtU*@{Vq{m3XaG0#{-7g;4k%lT6T&2%=-+G-cO@F9lDl!D56^k&dlr}vWd z3)cp35oLe;P3KRBz;9%+t@>v&q;$O)GSeus(NAR!$@$n~3 zYm0G888yzC{d&?7IR^D!di$C=s2EXq9q@D(uCwEIW%?bw=sl${}4j>;f*92u!E5sApCC2kyAqP$lSp*9|pRRpP@>zDE zT$-GJ)st9?SI!^u(=e4ikF@5&McG(CYeCn^!u61h{=T&Dz8uiaJhp#x^$0FYEjX6GX__w+ zRyNIOTZnCqgTzC;yPcq7x#Yn{Q}?Yyzmi^tEYcI^}L z#S34tJ;rKS_ZT-rPAMqI1zDy)p5vf@5$?aD{U-22$g%lo2%e&N8?IbKa zmN_?^l)A7_BGjE$o5xjYXay?55t^p!lO!r;0S_Bp9N6*z9Pd3n9#MCU(#%fpo!oh( z9+4HA5tOK#^9NH}Y+!p+i!SuftqRQ-?++=Up^|*TE8~g*B>w6@a}~Z(q&hQ!+*n!j zvDR@}iK3Fp6y#z^Z@*{QhQnjmh+dR9Zbd{^1TQ}^{Ih|0Or{dzsz`5BY*W;2vTOIY zy!IRC9oyeOQy!A&uW^C$gy_DPd=8R7 z15l!1_}yyh|3V)^M;QTU7NTIq_V%FV=0ByMNYbGQdUMZu-G83KsztLnCv&=s@OAwL4vyyCeq+mvarW zuXS=N~5MSXK3 zv{SZ^Bn4x3|J3~!O370nV=HBYP0$@3E-Yd)psGM8GL92ct{JW-x>@Iw#6Q4D9YLU# zLRB)(J{reD;2OdFMx9xaOf7(UoX|34Um{&M-6AT#=8zkg!4Jbai+Yp6O=drS_1=`t z`K6+&{5glLQg6RypYHD7(Hz`PDGoEH8`%SbNrWSx3jm(@pRVg|`(;q|`$9%OxM6b^~< zY6ut{jDnpDRa-1mfJ-Srz#8N%YzCA>Uls%N^YX}gRi5k5;Bg2T&taE}>90-o%73MV zc?95B9ux?bcf!imSlDnRRX#H{3}q8OSM%8_+jhibIY%JyH2+EN1dht7Jdw?0jxG_F zL7A5X5H_)M8AiZS0IEGl0vN!_izWyeY28|D9V-9d{?w$^<@vcpMY#C>w@?0GfB&E4 px<8#8QB^;|H3y*ZEAw?YfIPPpB43~YAb|V_{D1v7{(t%Y{{vdb(0u>^ literal 0 HcmV?d00001 diff --git a/app/assets/images/sounds/sounds.ogg b/app/assets/images/sounds/sounds.ogg new file mode 100644 index 0000000000000000000000000000000000000000..aaf4e774a9640615d0b31e60dedfd2f8054cdf0f GIT binary patch literal 202697 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 O$l&Sf=d#Wzp$PyEn|G4{ literal 0 HcmV?d00001 diff --git a/app/assets/images/video_sprite.png b/app/assets/images/video_sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed4e1b31aeb4901df62b24f7c35d1ccc1fdb4c4 GIT binary patch literal 1232 zcmV;>1TXuEP){|bI+At6%JpK=zz-&-D0W=w)bb^`*N+*arL9zj4180DwFhR%!g$>LE^gF*Z z703C9WE1xl={-GzEbG&KI^CUqTsC~si6HZ^gdkjIvW?SKyh));=TMCcF?$|J>qt&kTWW1)OnX4%(l z0HYG2ReUyJDL%-kg|Ef|5RfeM19DJBImqgQh*-2yG{Wm5_Xh#3@j*l^+9%ymG=>iX#x7G}lw&Oi1<{no^vH1rw1RIE zuu|TS8DuBQcU-P9y>cX=6?~I`Pyq^!#l_A5pNaQo$3jN}TERaMz|ozwq5-=fTE5c4 zKiEYG!4<8ZbRAA;Ay_nV@g08fA? zz!TsJ@C0}QJOQ2nPk<-jBGZ3)Kj8WKxfK8Q_I6G6F7|!^girDP{eYG_@NeD^psg@I z7;Pm%Er-bOir*84i`3#8zmGOJjk^I^N^0%_p4<;$!1BBcM1ur3Y%uKY0|4>u-Tzm+2z{l^#@XPK8>I!gV&!=-vOY@!3_@-x1>JK-Tno#daFG93iHI6~2w@tGp$~ zaeiiX`^l6F_%0;W-V+pShp$ac151*Q@NK6-?HwT=b()87oxj&v u-!1W|JLCIpaZi9Jz!TsJxa9OlfB^uPC&9vh1A-C&0000 + * MIT Licensed. http://www.opensource.org/licenses/mit-license.php + * + * https://github.com/gregjacobs/Autolinker.js + */ +/** + * @class Autolinker + * @extends Object + * + * Utility class used to process a given string of text, and wrap the matches in + * the appropriate anchor (<a>) tags to turn them into links. + * + * Any of the configuration options may be provided in an Object (map) provided + * to the Autolinker constructor, which will configure how the {@link #link link()} + * method will process the links. + * + * For example: + * + * var autolinker = new Autolinker( { + * newWindow : false, + * truncate : 30 + * } ); + * + * var html = autolinker.link( "Joe went to www.yahoo.com" ); + * // produces: 'Joe went to yahoo.com' + * + * + * The {@link #static-link static link()} method may also be used to inline options into a single call, which may + * be more convenient for one-off uses. For example: + * + * var html = Autolinker.link( "Joe went to www.yahoo.com", { + * newWindow : false, + * truncate : 30 + * } ); + * // produces: 'Joe went to yahoo.com' + * + * + * ## Custom Replacements of Links + * + * If the configuration options do not provide enough flexibility, a {@link #replaceFn} + * may be provided to fully customize the output of Autolinker. This function is + * called once for each URL/Email/Phone#/Twitter Handle/Hashtag match that is + * encountered. + * + * For example: + * + * var input = "..."; // string with URLs, Email Addresses, Phone #s, Twitter Handles, and Hashtags + * + * var linkedText = Autolinker.link( input, { + * replaceFn : function( autolinker, match ) { + * console.log( "href = ", match.getAnchorHref() ); + * console.log( "text = ", match.getAnchorText() ); + * + * switch( match.getType() ) { + * case 'url' : + * console.log( "url: ", match.getUrl() ); + * + * if( match.getUrl().indexOf( 'mysite.com' ) === -1 ) { + * var tag = autolinker.getTagBuilder().build( match ); // returns an `Autolinker.HtmlTag` instance, which provides mutator methods for easy changes + * tag.setAttr( 'rel', 'nofollow' ); + * tag.addClass( 'external-link' ); + * + * return tag; + * + * } else { + * return true; // let Autolinker perform its normal anchor tag replacement + * } + * + * case 'email' : + * var email = match.getEmail(); + * console.log( "email: ", email ); + * + * if( email === "my@own.address" ) { + * return false; // don't auto-link this particular email address; leave as-is + * } else { + * return; // no return value will have Autolinker perform its normal anchor tag replacement (same as returning `true`) + * } + * + * case 'phone' : + * var phoneNumber = match.getPhoneNumber(); + * console.log( phoneNumber ); + * + * return '' + phoneNumber + ''; + * + * case 'twitter' : + * var twitterHandle = match.getTwitterHandle(); + * console.log( twitterHandle ); + * + * return '' + twitterHandle + ''; + * + * case 'hashtag' : + * var hashtag = match.getHashtag(); + * console.log( hashtag ); + * + * return '' + hashtag + ''; + * } + * } + * } ); + * + * + * The function may return the following values: + * + * - `true` (Boolean): Allow Autolinker to replace the match as it normally would. + * - `false` (Boolean): Do not replace the current match at all - leave as-is. + * - Any String: If a string is returned from the function, the string will be used directly as the replacement HTML for + * the match. + * - An {@link Autolinker.HtmlTag} instance, which can be used to build/modify an HTML tag before writing out its HTML text. + * + * @constructor + * @param {Object} [config] The configuration options for the Autolinker instance, specified in an Object (map). + */ +var Autolinker = function( cfg ) { + Autolinker.Util.assign( this, cfg ); // assign the properties of `cfg` onto the Autolinker instance. Prototype properties will be used for missing configs. + + // Validate the value of the `hashtag` cfg. + var hashtag = this.hashtag; + if( hashtag !== false && hashtag !== 'twitter' && hashtag !== 'facebook' ) { + throw new Error( "invalid `hashtag` cfg - see docs" ); + } +}; + +Autolinker.prototype = { + constructor : Autolinker, // fix constructor property + + /** + * @cfg {Boolean} urls + * + * `true` if miscellaneous URLs should be automatically linked, `false` if they should not be. + */ + urls : true, + + /** + * @cfg {Boolean} email + * + * `true` if email addresses should be automatically linked, `false` if they should not be. + */ + email : true, + + /** + * @cfg {Boolean} twitter + * + * `true` if Twitter handles ("@example") should be automatically linked, `false` if they should not be. + */ + twitter : true, + + /** + * @cfg {Boolean} phone + * + * `true` if Phone numbers ("(555)555-5555") should be automatically linked, `false` if they should not be. + */ + phone: true, + + /** + * @cfg {Boolean/String} hashtag + * + * A string for the service name to have hashtags (ex: "#myHashtag") + * auto-linked to. The currently-supported values are: + * + * - 'twitter' + * - 'facebook' + * + * Pass `false` to skip auto-linking of hashtags. + */ + hashtag : false, + + /** + * @cfg {Boolean} newWindow + * + * `true` if the links should open in a new window, `false` otherwise. + */ + newWindow : true, + + /** + * @cfg {Boolean} stripPrefix + * + * `true` if 'http://' or 'https://' and/or the 'www.' should be stripped + * from the beginning of URL links' text, `false` otherwise. + */ + stripPrefix : true, + + /** + * @cfg {Number} truncate + * + * A number for how many characters long matched text should be truncated to inside the text of + * a link. If the matched text is over this number of characters, it will be truncated to this length by + * adding a two period ellipsis ('..') to the end of the string. + * + * For example: A url like 'http://www.yahoo.com/some/long/path/to/a/file' truncated to 25 characters might look + * something like this: 'yahoo.com/some/long/pat..' + */ + truncate : undefined, + + /** + * @cfg {String} className + * + * A CSS class name to add to the generated links. This class will be added to all links, as well as this class + * plus match suffixes for styling url/email/phone/twitter/hashtag links differently. + * + * For example, if this config is provided as "myLink", then: + * + * - URL links will have the CSS classes: "myLink myLink-url" + * - Email links will have the CSS classes: "myLink myLink-email", and + * - Twitter links will have the CSS classes: "myLink myLink-twitter" + * - Phone links will have the CSS classes: "myLink myLink-phone" + * - Hashtag links will have the CSS classes: "myLink myLink-hashtag" + */ + className : "", + + /** + * @cfg {Function} replaceFn + * + * A function to individually process each match found in the input string. + * + * See the class's description for usage. + * + * This function is called with the following parameters: + * + * @cfg {Autolinker} replaceFn.autolinker The Autolinker instance, which may be used to retrieve child objects from (such + * as the instance's {@link #getTagBuilder tag builder}). + * @cfg {Autolinker.match.Match} replaceFn.match The Match instance which can be used to retrieve information about the + * match that the `replaceFn` is currently processing. See {@link Autolinker.match.Match} subclasses for details. + */ + + + /** + * @private + * @property {Autolinker.htmlParser.HtmlParser} htmlParser + * + * The HtmlParser instance used to skip over HTML tags, while finding text nodes to process. This is lazily instantiated + * in the {@link #getHtmlParser} method. + */ + htmlParser : undefined, + + /** + * @private + * @property {Autolinker.matchParser.MatchParser} matchParser + * + * The MatchParser instance used to find matches in the text nodes of an input string passed to + * {@link #link}. This is lazily instantiated in the {@link #getMatchParser} method. + */ + matchParser : undefined, + + /** + * @private + * @property {Autolinker.AnchorTagBuilder} tagBuilder + * + * The AnchorTagBuilder instance used to build match replacement anchor tags. Note: this is lazily instantiated + * in the {@link #getTagBuilder} method. + */ + tagBuilder : undefined, + + /** + * Automatically links URLs, Email addresses, Phone numbers, Twitter + * handles, and Hashtags found in the given chunk of HTML. Does not link + * URLs found within HTML tags. + * + * For instance, if given the text: `You should go to http://www.yahoo.com`, + * then the result will be `You should go to + * <a href="http://www.yahoo.com">http://www.yahoo.com</a>` + * + * This method finds the text around any HTML elements in the input + * `textOrHtml`, which will be the text that is processed. Any original HTML + * elements will be left as-is, as well as the text that is already wrapped + * in anchor (<a>) tags. + * + * @param {String} textOrHtml The HTML or text to autolink matches within + * (depending on if the {@link #urls}, {@link #email}, {@link #phone}, + * {@link #twitter}, and {@link #hashtag} options are enabled). + * @return {String} The HTML, with matches automatically linked. + */ + link : function( textOrHtml ) { + var htmlParser = this.getHtmlParser(), + htmlNodes = htmlParser.parse( textOrHtml ), + anchorTagStackCount = 0, // used to only process text around anchor tags, and any inner text/html they may have + resultHtml = []; + + for( var i = 0, len = htmlNodes.length; i < len; i++ ) { + var node = htmlNodes[ i ], + nodeType = node.getType(), + nodeText = node.getText(); + + if( nodeType === 'element' ) { + // Process HTML nodes in the input `textOrHtml` + if( node.getTagName() === 'a' ) { + if( !node.isClosing() ) { // it's the start tag + anchorTagStackCount++; + } else { // it's the end tag + anchorTagStackCount = Math.max( anchorTagStackCount - 1, 0 ); // attempt to handle extraneous tags by making sure the stack count never goes below 0 + } + } + resultHtml.push( nodeText ); // now add the text of the tag itself verbatim + + } else if( nodeType === 'entity' || nodeType === 'comment' ) { + resultHtml.push( nodeText ); // append HTML entity nodes (such as ' ') or HTML comments (such as '') verbatim + + } else { + // Process text nodes in the input `textOrHtml` + if( anchorTagStackCount === 0 ) { + // If we're not within an tag, process the text node to linkify + var linkifiedStr = this.linkifyStr( nodeText ); + resultHtml.push( linkifiedStr ); + + } else { + // `text` is within an tag, simply append the text - we do not want to autolink anything + // already within an ... tag + resultHtml.push( nodeText ); + } + } + } + + return resultHtml.join( "" ); + }, + + /** + * Process the text that lies in between HTML tags, performing the anchor + * tag replacements for the matches, and returns the string with the + * replacements made. + * + * This method does the actual wrapping of matches with anchor tags. + * + * @private + * @param {String} str The string of text to auto-link. + * @return {String} The text with anchor tags auto-filled. + */ + linkifyStr : function( str ) { + return this.getMatchParser().replace( str, this.createMatchReturnVal, this ); + }, + + + /** + * Creates the return string value for a given match in the input string, + * for the {@link #linkifyStr} method. + * + * This method handles the {@link #replaceFn}, if one was provided. + * + * @private + * @param {Autolinker.match.Match} match The Match object that represents the match. + * @return {String} The string that the `match` should be replaced with. This is usually the anchor tag string, but + * may be the `matchStr` itself if the match is not to be replaced. + */ + createMatchReturnVal : function( match ) { + // Handle a custom `replaceFn` being provided + var replaceFnResult; + if( this.replaceFn ) { + replaceFnResult = this.replaceFn.call( this, this, match ); // Autolinker instance is the context, and the first arg + } + + if( typeof replaceFnResult === 'string' ) { + return replaceFnResult; // `replaceFn` returned a string, use that + + } else if( replaceFnResult === false ) { + return match.getMatchedText(); // no replacement for the match + + } else if( replaceFnResult instanceof Autolinker.HtmlTag ) { + return replaceFnResult.toAnchorString(); + + } else { // replaceFnResult === true, or no/unknown return value from function + // Perform Autolinker's default anchor tag generation + var tagBuilder = this.getTagBuilder(), + anchorTag = tagBuilder.build( match ); // returns an Autolinker.HtmlTag instance + + return anchorTag.toAnchorString(); + } + }, + + + /** + * Lazily instantiates and returns the {@link #htmlParser} instance for this Autolinker instance. + * + * @protected + * @return {Autolinker.htmlParser.HtmlParser} + */ + getHtmlParser : function() { + var htmlParser = this.htmlParser; + + if( !htmlParser ) { + htmlParser = this.htmlParser = new Autolinker.htmlParser.HtmlParser(); + } + + return htmlParser; + }, + + + /** + * Lazily instantiates and returns the {@link #matchParser} instance for this Autolinker instance. + * + * @protected + * @return {Autolinker.matchParser.MatchParser} + */ + getMatchParser : function() { + var matchParser = this.matchParser; + + if( !matchParser ) { + matchParser = this.matchParser = new Autolinker.matchParser.MatchParser( { + urls : this.urls, + email : this.email, + twitter : this.twitter, + phone : this.phone, + hashtag : this.hashtag, + stripPrefix : this.stripPrefix + } ); + } + + return matchParser; + }, + + + /** + * Returns the {@link #tagBuilder} instance for this Autolinker instance, lazily instantiating it + * if it does not yet exist. + * + * This method may be used in a {@link #replaceFn} to generate the {@link Autolinker.HtmlTag HtmlTag} instance that + * Autolinker would normally generate, and then allow for modifications before returning it. For example: + * + * var html = Autolinker.link( "Test google.com", { + * replaceFn : function( autolinker, match ) { + * var tag = autolinker.getTagBuilder().build( match ); // returns an {@link Autolinker.HtmlTag} instance + * tag.setAttr( 'rel', 'nofollow' ); + * + * return tag; + * } + * } ); + * + * // generated html: + * // Test google.com + * + * @return {Autolinker.AnchorTagBuilder} + */ + getTagBuilder : function() { + var tagBuilder = this.tagBuilder; + + if( !tagBuilder ) { + tagBuilder = this.tagBuilder = new Autolinker.AnchorTagBuilder( { + newWindow : this.newWindow, + truncate : this.truncate, + className : this.className + } ); + } + + return tagBuilder; + } + +}; + + +/** + * Automatically links URLs, Email addresses, Phone Numbers, Twitter handles, + * and Hashtags found in the given chunk of HTML. Does not link URLs found + * within HTML tags. + * + * For instance, if given the text: `You should go to http://www.yahoo.com`, + * then the result will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` + * + * Example: + * + * var linkedText = Autolinker.link( "Go to google.com", { newWindow: false } ); + * // Produces: "Go to google.com" + * + * @static + * @param {String} textOrHtml The HTML or text to find matches within (depending + * on if the {@link #urls}, {@link #email}, {@link #phone}, {@link #twitter}, + * and {@link #hashtag} options are enabled). + * @param {Object} [options] Any of the configuration options for the Autolinker + * class, specified in an Object (map). See the class description for an + * example call. + * @return {String} The HTML text, with matches automatically linked. + */ +Autolinker.link = function( textOrHtml, options ) { + var autolinker = new Autolinker( options ); + return autolinker.link( textOrHtml ); +}; + + +// Autolinker Namespaces +Autolinker.match = {}; +Autolinker.htmlParser = {}; +Autolinker.matchParser = {}; + +/*global Autolinker */ +/*jshint eqnull:true, boss:true */ +/** + * @class Autolinker.Util + * @singleton + * + * A few utility methods for Autolinker. + */ +Autolinker.Util = { + + /** + * @property {Function} abstractMethod + * + * A function object which represents an abstract method. + */ + abstractMethod : function() { throw "abstract"; }, + + + /** + * @private + * @property {RegExp} trimRegex + * + * The regular expression used to trim the leading and trailing whitespace + * from a string. + */ + trimRegex : /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + + /** + * Assigns (shallow copies) the properties of `src` onto `dest`. + * + * @param {Object} dest The destination object. + * @param {Object} src The source object. + * @return {Object} The destination object (`dest`) + */ + assign : function( dest, src ) { + for( var prop in src ) { + if( src.hasOwnProperty( prop ) ) { + dest[ prop ] = src[ prop ]; + } + } + + return dest; + }, + + + /** + * Extends `superclass` to create a new subclass, adding the `protoProps` to the new subclass's prototype. + * + * @param {Function} superclass The constructor function for the superclass. + * @param {Object} protoProps The methods/properties to add to the subclass's prototype. This may contain the + * special property `constructor`, which will be used as the new subclass's constructor function. + * @return {Function} The new subclass function. + */ + extend : function( superclass, protoProps ) { + var superclassProto = superclass.prototype; + + var F = function() {}; + F.prototype = superclassProto; + + var subclass; + if( protoProps.hasOwnProperty( 'constructor' ) ) { + subclass = protoProps.constructor; + } else { + subclass = function() { superclassProto.constructor.apply( this, arguments ); }; + } + + var subclassProto = subclass.prototype = new F(); // set up prototype chain + subclassProto.constructor = subclass; // fix constructor property + subclassProto.superclass = superclassProto; + + delete protoProps.constructor; // don't re-assign constructor property to the prototype, since a new function may have been created (`subclass`), which is now already there + Autolinker.Util.assign( subclassProto, protoProps ); + + return subclass; + }, + + + /** + * Truncates the `str` at `len - ellipsisChars.length`, and adds the `ellipsisChars` to the + * end of the string (by default, two periods: '..'). If the `str` length does not exceed + * `len`, the string will be returned unchanged. + * + * @param {String} str The string to truncate and add an ellipsis to. + * @param {Number} truncateLen The length to truncate the string at. + * @param {String} [ellipsisChars=..] The ellipsis character(s) to add to the end of `str` + * when truncated. Defaults to '..' + */ + ellipsis : function( str, truncateLen, ellipsisChars ) { + if( str.length > truncateLen ) { + ellipsisChars = ( ellipsisChars == null ) ? '..' : ellipsisChars; + str = str.substring( 0, truncateLen - ellipsisChars.length ) + ellipsisChars; + } + return str; + }, + + + /** + * Supports `Array.prototype.indexOf()` functionality for old IE (IE8 and below). + * + * @param {Array} arr The array to find an element of. + * @param {*} element The element to find in the array, and return the index of. + * @return {Number} The index of the `element`, or -1 if it was not found. + */ + indexOf : function( arr, element ) { + if( Array.prototype.indexOf ) { + return arr.indexOf( element ); + + } else { + for( var i = 0, len = arr.length; i < len; i++ ) { + if( arr[ i ] === element ) return i; + } + return -1; + } + }, + + + + /** + * Performs the functionality of what modern browsers do when `String.prototype.split()` is called + * with a regular expression that contains capturing parenthesis. + * + * For example: + * + * // Modern browsers: + * "a,b,c".split( /(,)/ ); // --> [ 'a', ',', 'b', ',', 'c' ] + * + * // Old IE (including IE8): + * "a,b,c".split( /(,)/ ); // --> [ 'a', 'b', 'c' ] + * + * This method emulates the functionality of modern browsers for the old IE case. + * + * @param {String} str The string to split. + * @param {RegExp} splitRegex The regular expression to split the input `str` on. The splitting + * character(s) will be spliced into the array, as in the "modern browsers" example in the + * description of this method. + * Note #1: the supplied regular expression **must** have the 'g' flag specified. + * Note #2: for simplicity's sake, the regular expression does not need + * to contain capturing parenthesis - it will be assumed that any match has them. + * @return {String[]} The split array of strings, with the splitting character(s) included. + */ + splitAndCapture : function( str, splitRegex ) { + if( !splitRegex.global ) throw new Error( "`splitRegex` must have the 'g' flag set" ); + + var result = [], + lastIdx = 0, + match; + + while( match = splitRegex.exec( str ) ) { + result.push( str.substring( lastIdx, match.index ) ); + result.push( match[ 0 ] ); // push the splitting char(s) + + lastIdx = match.index + match[ 0 ].length; + } + result.push( str.substring( lastIdx ) ); + + return result; + }, + + + /** + * Trims the leading and trailing whitespace from a string. + * + * @param {String} str The string to trim. + * @return {String} + */ + trim : function( str ) { + return str.replace( this.trimRegex, '' ); + } + +}; +/*global Autolinker */ +/*jshint boss:true */ +/** + * @class Autolinker.HtmlTag + * @extends Object + * + * Represents an HTML tag, which can be used to easily build/modify HTML tags programmatically. + * + * Autolinker uses this abstraction to create HTML tags, and then write them out as strings. You may also use + * this class in your code, especially within a {@link Autolinker#replaceFn replaceFn}. + * + * ## Examples + * + * Example instantiation: + * + * var tag = new Autolinker.HtmlTag( { + * tagName : 'a', + * attrs : { 'href': 'http://google.com', 'class': 'external-link' }, + * innerHtml : 'Google' + * } ); + * + * tag.toAnchorString(); // Google + * + * // Individual accessor methods + * tag.getTagName(); // 'a' + * tag.getAttr( 'href' ); // 'http://google.com' + * tag.hasClass( 'external-link' ); // true + * + * + * Using mutator methods (which may be used in combination with instantiation config properties): + * + * var tag = new Autolinker.HtmlTag(); + * tag.setTagName( 'a' ); + * tag.setAttr( 'href', 'http://google.com' ); + * tag.addClass( 'external-link' ); + * tag.setInnerHtml( 'Google' ); + * + * tag.getTagName(); // 'a' + * tag.getAttr( 'href' ); // 'http://google.com' + * tag.hasClass( 'external-link' ); // true + * + * tag.toAnchorString(); // Google + * + * + * ## Example use within a {@link Autolinker#replaceFn replaceFn} + * + * var html = Autolinker.link( "Test google.com", { + * replaceFn : function( autolinker, match ) { + * var tag = autolinker.getTagBuilder().build( match ); // returns an {@link Autolinker.HtmlTag} instance, configured with the Match's href and anchor text + * tag.setAttr( 'rel', 'nofollow' ); + * + * return tag; + * } + * } ); + * + * // generated html: + * // Test google.com + * + * + * ## Example use with a new tag for the replacement + * + * var html = Autolinker.link( "Test google.com", { + * replaceFn : function( autolinker, match ) { + * var tag = new Autolinker.HtmlTag( { + * tagName : 'button', + * attrs : { 'title': 'Load URL: ' + match.getAnchorHref() }, + * innerHtml : 'Load URL: ' + match.getAnchorText() + * } ); + * + * return tag; + * } + * } ); + * + * // generated html: + * // Test + */ +Autolinker.HtmlTag = Autolinker.Util.extend( Object, { + + /** + * @cfg {String} tagName + * + * The tag name. Ex: 'a', 'button', etc. + * + * Not required at instantiation time, but should be set using {@link #setTagName} before {@link #toAnchorString} + * is executed. + */ + + /** + * @cfg {Object.} attrs + * + * An key/value Object (map) of attributes to create the tag with. The keys are the attribute names, and the + * values are the attribute values. + */ + + /** + * @cfg {String} innerHtml + * + * The inner HTML for the tag. + * + * Note the camel case name on `innerHtml`. Acronyms are camelCased in this utility (such as not to run into the acronym + * naming inconsistency that the DOM developers created with `XMLHttpRequest`). You may alternatively use {@link #innerHTML} + * if you prefer, but this one is recommended. + */ + + /** + * @cfg {String} innerHTML + * + * Alias of {@link #innerHtml}, accepted for consistency with the browser DOM api, but prefer the camelCased version + * for acronym names. + */ + + + /** + * @protected + * @property {RegExp} whitespaceRegex + * + * Regular expression used to match whitespace in a string of CSS classes. + */ + whitespaceRegex : /\s+/, + + + /** + * @constructor + * @param {Object} [cfg] The configuration properties for this class, in an Object (map) + */ + constructor : function( cfg ) { + Autolinker.Util.assign( this, cfg ); + + this.innerHtml = this.innerHtml || this.innerHTML; // accept either the camelCased form or the fully capitalized acronym + }, + + + /** + * Sets the tag name that will be used to generate the tag with. + * + * @param {String} tagName + * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. + */ + setTagName : function( tagName ) { + this.tagName = tagName; + return this; + }, + + + /** + * Retrieves the tag name. + * + * @return {String} + */ + getTagName : function() { + return this.tagName || ""; + }, + + + /** + * Sets an attribute on the HtmlTag. + * + * @param {String} attrName The attribute name to set. + * @param {String} attrValue The attribute value to set. + * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. + */ + setAttr : function( attrName, attrValue ) { + var tagAttrs = this.getAttrs(); + tagAttrs[ attrName ] = attrValue; + + return this; + }, + + + /** + * Retrieves an attribute from the HtmlTag. If the attribute does not exist, returns `undefined`. + * + * @param {String} name The attribute name to retrieve. + * @return {String} The attribute's value, or `undefined` if it does not exist on the HtmlTag. + */ + getAttr : function( attrName ) { + return this.getAttrs()[ attrName ]; + }, + + + /** + * Sets one or more attributes on the HtmlTag. + * + * @param {Object.} attrs A key/value Object (map) of the attributes to set. + * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. + */ + setAttrs : function( attrs ) { + var tagAttrs = this.getAttrs(); + Autolinker.Util.assign( tagAttrs, attrs ); + + return this; + }, + + + /** + * Retrieves the attributes Object (map) for the HtmlTag. + * + * @return {Object.} A key/value object of the attributes for the HtmlTag. + */ + getAttrs : function() { + return this.attrs || ( this.attrs = {} ); + }, + + + /** + * Sets the provided `cssClass`, overwriting any current CSS classes on the HtmlTag. + * + * @param {String} cssClass One or more space-separated CSS classes to set (overwrite). + * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. + */ + setClass : function( cssClass ) { + return this.setAttr( 'class', cssClass ); + }, + + + /** + * Convenience method to add one or more CSS classes to the HtmlTag. Will not add duplicate CSS classes. + * + * @param {String} cssClass One or more space-separated CSS classes to add. + * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. + */ + addClass : function( cssClass ) { + var classAttr = this.getClass(), + whitespaceRegex = this.whitespaceRegex, + indexOf = Autolinker.Util.indexOf, // to support IE8 and below + classes = ( !classAttr ) ? [] : classAttr.split( whitespaceRegex ), + newClasses = cssClass.split( whitespaceRegex ), + newClass; + + while( newClass = newClasses.shift() ) { + if( indexOf( classes, newClass ) === -1 ) { + classes.push( newClass ); + } + } + + this.getAttrs()[ 'class' ] = classes.join( " " ); + return this; + }, + + + /** + * Convenience method to remove one or more CSS classes from the HtmlTag. + * + * @param {String} cssClass One or more space-separated CSS classes to remove. + * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. + */ + removeClass : function( cssClass ) { + var classAttr = this.getClass(), + whitespaceRegex = this.whitespaceRegex, + indexOf = Autolinker.Util.indexOf, // to support IE8 and below + classes = ( !classAttr ) ? [] : classAttr.split( whitespaceRegex ), + removeClasses = cssClass.split( whitespaceRegex ), + removeClass; + + while( classes.length && ( removeClass = removeClasses.shift() ) ) { + var idx = indexOf( classes, removeClass ); + if( idx !== -1 ) { + classes.splice( idx, 1 ); + } + } + + this.getAttrs()[ 'class' ] = classes.join( " " ); + return this; + }, + + + /** + * Convenience method to retrieve the CSS class(es) for the HtmlTag, which will each be separated by spaces when + * there are multiple. + * + * @return {String} + */ + getClass : function() { + return this.getAttrs()[ 'class' ] || ""; + }, + + + /** + * Convenience method to check if the tag has a CSS class or not. + * + * @param {String} cssClass The CSS class to check for. + * @return {Boolean} `true` if the HtmlTag has the CSS class, `false` otherwise. + */ + hasClass : function( cssClass ) { + return ( ' ' + this.getClass() + ' ' ).indexOf( ' ' + cssClass + ' ' ) !== -1; + }, + + + /** + * Sets the inner HTML for the tag. + * + * @param {String} html The inner HTML to set. + * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. + */ + setInnerHtml : function( html ) { + this.innerHtml = html; + + return this; + }, + + + /** + * Retrieves the inner HTML for the tag. + * + * @return {String} + */ + getInnerHtml : function() { + return this.innerHtml || ""; + }, + + + /** + * Override of superclass method used to generate the HTML string for the tag. + * + * @return {String} + */ + toAnchorString : function() { + var tagName = this.getTagName(), + attrsStr = this.buildAttrsStr(); + + attrsStr = ( attrsStr ) ? ' ' + attrsStr : ''; // prepend a space if there are actually attributes + + return [ '<', tagName, attrsStr, '>', this.getInnerHtml(), '' ].join( "" ); + }, + + + /** + * Support method for {@link #toAnchorString}, returns the string space-separated key="value" pairs, used to populate + * the stringified HtmlTag. + * + * @protected + * @return {String} Example return: `attr1="value1" attr2="value2"` + */ + buildAttrsStr : function() { + if( !this.attrs ) return ""; // no `attrs` Object (map) has been set, return empty string + + var attrs = this.getAttrs(), + attrsArr = []; + + for( var prop in attrs ) { + if( attrs.hasOwnProperty( prop ) ) { + attrsArr.push( prop + '="' + attrs[ prop ] + '"' ); + } + } + return attrsArr.join( " " ); + } + +} ); + +/*global Autolinker */ +/*jshint sub:true */ +/** + * @protected + * @class Autolinker.AnchorTagBuilder + * @extends Object + * + * Builds anchor (<a>) tags for the Autolinker utility when a match is found. + * + * Normally this class is instantiated, configured, and used internally by an {@link Autolinker} instance, but may + * actually be retrieved in a {@link Autolinker#replaceFn replaceFn} to create {@link Autolinker.HtmlTag HtmlTag} instances + * which may be modified before returning from the {@link Autolinker#replaceFn replaceFn}. For example: + * + * var html = Autolinker.link( "Test google.com", { + * replaceFn : function( autolinker, match ) { + * var tag = autolinker.getTagBuilder().build( match ); // returns an {@link Autolinker.HtmlTag} instance + * tag.setAttr( 'rel', 'nofollow' ); + * + * return tag; + * } + * } ); + * + * // generated html: + * // Test google.com + */ +Autolinker.AnchorTagBuilder = Autolinker.Util.extend( Object, { + + /** + * @cfg {Boolean} newWindow + * @inheritdoc Autolinker#newWindow + */ + + /** + * @cfg {Number} truncate + * @inheritdoc Autolinker#truncate + */ + + /** + * @cfg {String} className + * @inheritdoc Autolinker#className + */ + + + /** + * @constructor + * @param {Object} [cfg] The configuration options for the AnchorTagBuilder instance, specified in an Object (map). + */ + constructor : function( cfg ) { + Autolinker.Util.assign( this, cfg ); + }, + + + /** + * Generates the actual anchor (<a>) tag to use in place of the + * matched text, via its `match` object. + * + * @param {Autolinker.match.Match} match The Match instance to generate an + * anchor tag from. + * @return {Autolinker.HtmlTag} The HtmlTag instance for the anchor tag. + */ + build : function( match ) { + var tag = new Autolinker.HtmlTag( { + tagName : 'a', + attrs : this.createAttrs( match.getType(), match.getAnchorHref() ), + innerHtml : this.processAnchorText( match.getAnchorText() ) + } ); + + return tag; + }, + + + /** + * Creates the Object (map) of the HTML attributes for the anchor (<a>) + * tag being generated. + * + * @protected + * @param {"url"/"email"/"phone"/"twitter"/"hashtag"} matchType The type of + * match that an anchor tag is being generated for. + * @param {String} href The href for the anchor tag. + * @return {Object} A key/value Object (map) of the anchor tag's attributes. + */ + createAttrs : function( matchType, anchorHref ) { + var attrs = { + 'href' : anchorHref // we'll always have the `href` attribute + }; + + var cssClass = this.createCssClass( matchType ); + if( cssClass ) { + attrs[ 'class' ] = cssClass; + } + if( this.newWindow ) { + attrs[ 'target' ] = "_blank"; + } + + return attrs; + }, + + + /** + * Creates the CSS class that will be used for a given anchor tag, based on + * the `matchType` and the {@link #className} config. + * + * @private + * @param {"url"/"email"/"phone"/"twitter"/"hashtag"} matchType The type of + * match that an anchor tag is being generated for. + * @return {String} The CSS class string for the link. Example return: + * "myLink myLink-url". If no {@link #className} was configured, returns + * an empty string. + */ + createCssClass : function( matchType ) { + var className = this.className; + + if( !className ) + return ""; + else + return className + " " + className + "-" + matchType; // ex: "myLink myLink-url", "myLink myLink-email", "myLink myLink-phone", "myLink myLink-twitter", or "myLink myLink-hashtag" + }, + + + /** + * Processes the `anchorText` by truncating the text according to the + * {@link #truncate} config. + * + * @private + * @param {String} anchorText The anchor tag's text (i.e. what will be + * displayed). + * @return {String} The processed `anchorText`. + */ + processAnchorText : function( anchorText ) { + anchorText = this.doTruncate( anchorText ); + + return anchorText; + }, + + + /** + * Performs the truncation of the `anchorText`, if the `anchorText` is + * longer than the {@link #truncate} option. Truncates the text to 2 + * characters fewer than the {@link #truncate} option, and adds ".." to the + * end. + * + * @private + * @param {String} text The anchor tag's text (i.e. what will be displayed). + * @return {String} The truncated anchor text. + */ + doTruncate : function( anchorText ) { + return Autolinker.Util.ellipsis( anchorText, this.truncate || Number.POSITIVE_INFINITY ); + } + +} ); +/*global Autolinker */ +/** + * @private + * @class Autolinker.htmlParser.HtmlParser + * @extends Object + * + * An HTML parser implementation which simply walks an HTML string and returns an array of + * {@link Autolinker.htmlParser.HtmlNode HtmlNodes} that represent the basic HTML structure of the input string. + * + * Autolinker uses this to only link URLs/emails/Twitter handles within text nodes, effectively ignoring / "walking + * around" HTML tags. + */ +Autolinker.htmlParser.HtmlParser = Autolinker.Util.extend( Object, { + + /** + * @private + * @property {RegExp} htmlRegex + * + * The regular expression used to pull out HTML tags from a string. Handles namespaced HTML tags and + * attribute names, as specified by http://www.w3.org/TR/html-markup/syntax.html. + * + * Capturing groups: + * + * 1. The "!DOCTYPE" tag name, if a tag is a <!DOCTYPE> tag. + * 2. If it is an end tag, this group will have the '/'. + * 3. If it is a comment tag, this group will hold the comment text (i.e. + * the text inside the `<!--` and `-->`. + * 4. The tag name for all tags (other than the <!DOCTYPE> tag) + */ + htmlRegex : (function() { + var commentTagRegex = /!--([\s\S]+?)--/, + tagNameRegex = /[0-9a-zA-Z][0-9a-zA-Z:]*/, + attrNameRegex = /[^\s\0"'>\/=\x01-\x1F\x7F]+/, // the unicode range accounts for excluding control chars, and the delete char + attrValueRegex = /(?:"[^"]*?"|'[^']*?'|[^'"=<>`\s]+)/, // double quoted, single quoted, or unquoted attribute values + nameEqualsValueRegex = attrNameRegex.source + '(?:\\s*=\\s*' + attrValueRegex.source + ')?'; // optional '=[value]' + + return new RegExp( [ + // for tag. Ex: ) + '(?:', + '<(!DOCTYPE)', // *** Capturing Group 1 - If it's a doctype tag + + // Zero or more attributes following the tag name + '(?:', + '\\s+', // one or more whitespace chars before an attribute + + // Either: + // A. attr="value", or + // B. "value" alone (To cover example doctype tag: ) + '(?:', nameEqualsValueRegex, '|', attrValueRegex.source + ')', + ')*', + '>', + ')', + + '|', + + // All other HTML tags (i.e. tags that are not ) + '(?:', + '<(/)?', // Beginning of a tag or comment. Either '<' for a start tag, or '' + + ')', + ')', + '>', + ')' + ].join( "" ), 'gi' ); + } )(), + + /** + * @private + * @property {RegExp} htmlCharacterEntitiesRegex + * + * The regular expression that matches common HTML character entities. + * + * Ignoring & as it could be part of a query string -- handling it separately. + */ + htmlCharacterEntitiesRegex: /( | |<|<|>|>|"|"|')/gi, + + + /** + * Parses an HTML string and returns a simple array of {@link Autolinker.htmlParser.HtmlNode HtmlNodes} + * to represent the HTML structure of the input string. + * + * @param {String} html The HTML to parse. + * @return {Autolinker.htmlParser.HtmlNode[]} + */ + parse : function( html ) { + var htmlRegex = this.htmlRegex, + currentResult, + lastIndex = 0, + textAndEntityNodes, + nodes = []; // will be the result of the method + + while( ( currentResult = htmlRegex.exec( html ) ) !== null ) { + var tagText = currentResult[ 0 ], + commentText = currentResult[ 3 ], // if we've matched a comment + tagName = currentResult[ 1 ] || currentResult[ 4 ], // The tag (ex: "!DOCTYPE"), or another tag (ex: "a" or "img") + isClosingTag = !!currentResult[ 2 ], + inBetweenTagsText = html.substring( lastIndex, currentResult.index ); + + // Push TextNodes and EntityNodes for any text found between tags + if( inBetweenTagsText ) { + textAndEntityNodes = this.parseTextAndEntityNodes( inBetweenTagsText ); + nodes.push.apply( nodes, textAndEntityNodes ); + } + + // Push the CommentNode or ElementNode + if( commentText ) { + nodes.push( this.createCommentNode( tagText, commentText ) ); + } else { + nodes.push( this.createElementNode( tagText, tagName, isClosingTag ) ); + } + + lastIndex = currentResult.index + tagText.length; + } + + // Process any remaining text after the last HTML element. Will process all of the text if there were no HTML elements. + if( lastIndex < html.length ) { + var text = html.substring( lastIndex ); + + // Push TextNodes and EntityNodes for any text found between tags + if( text ) { + textAndEntityNodes = this.parseTextAndEntityNodes( text ); + nodes.push.apply( nodes, textAndEntityNodes ); + } + } + + return nodes; + }, + + + /** + * Parses text and HTML entity nodes from a given string. The input string + * should not have any HTML tags (elements) within it. + * + * @private + * @param {String} text The text to parse. + * @return {Autolinker.htmlParser.HtmlNode[]} An array of HtmlNodes to + * represent the {@link Autolinker.htmlParser.TextNode TextNodes} and + * {@link Autolinker.htmlParser.EntityNode EntityNodes} found. + */ + parseTextAndEntityNodes : function( text ) { + var nodes = [], + textAndEntityTokens = Autolinker.Util.splitAndCapture( text, this.htmlCharacterEntitiesRegex ); // split at HTML entities, but include the HTML entities in the results array + + // Every even numbered token is a TextNode, and every odd numbered token is an EntityNode + // For example: an input `text` of "Test "this" today" would turn into the + // `textAndEntityTokens`: [ 'Test ', '"', 'this', '"', ' today' ] + for( var i = 0, len = textAndEntityTokens.length; i < len; i += 2 ) { + var textToken = textAndEntityTokens[ i ], + entityToken = textAndEntityTokens[ i + 1 ]; + + if( textToken ) nodes.push( this.createTextNode( textToken ) ); + if( entityToken ) nodes.push( this.createEntityNode( entityToken ) ); + } + return nodes; + }, + + + /** + * Factory method to create an {@link Autolinker.htmlParser.CommentNode CommentNode}. + * + * @private + * @param {String} tagText The full text of the tag (comment) that was + * matched, including its <!-- and -->. + * @param {String} comment The full text of the comment that was matched. + */ + createCommentNode : function( tagText, commentText ) { + return new Autolinker.htmlParser.CommentNode( { + text: tagText, + comment: Autolinker.Util.trim( commentText ) + } ); + }, + + + /** + * Factory method to create an {@link Autolinker.htmlParser.ElementNode ElementNode}. + * + * @private + * @param {String} tagText The full text of the tag (element) that was + * matched, including its attributes. + * @param {String} tagName The name of the tag. Ex: An <img> tag would + * be passed to this method as "img". + * @param {Boolean} isClosingTag `true` if it's a closing tag, false + * otherwise. + * @return {Autolinker.htmlParser.ElementNode} + */ + createElementNode : function( tagText, tagName, isClosingTag ) { + return new Autolinker.htmlParser.ElementNode( { + text : tagText, + tagName : tagName.toLowerCase(), + closing : isClosingTag + } ); + }, + + + /** + * Factory method to create a {@link Autolinker.htmlParser.EntityNode EntityNode}. + * + * @private + * @param {String} text The text that was matched for the HTML entity (such + * as '&nbsp;'). + * @return {Autolinker.htmlParser.EntityNode} + */ + createEntityNode : function( text ) { + return new Autolinker.htmlParser.EntityNode( { text: text } ); + }, + + + /** + * Factory method to create a {@link Autolinker.htmlParser.TextNode TextNode}. + * + * @private + * @param {String} text The text that was matched. + * @return {Autolinker.htmlParser.TextNode} + */ + createTextNode : function( text ) { + return new Autolinker.htmlParser.TextNode( { text: text } ); + } + +} ); +/*global Autolinker */ +/** + * @abstract + * @class Autolinker.htmlParser.HtmlNode + * + * Represents an HTML node found in an input string. An HTML node is one of the following: + * + * 1. An {@link Autolinker.htmlParser.ElementNode ElementNode}, which represents HTML tags. + * 2. A {@link Autolinker.htmlParser.TextNode TextNode}, which represents text outside or within HTML tags. + * 3. A {@link Autolinker.htmlParser.EntityNode EntityNode}, which represents one of the known HTML + * entities that Autolinker looks for. This includes common ones such as &quot; and &nbsp; + */ +Autolinker.htmlParser.HtmlNode = Autolinker.Util.extend( Object, { + + /** + * @cfg {String} text (required) + * + * The original text that was matched for the HtmlNode. + * + * - In the case of an {@link Autolinker.htmlParser.ElementNode ElementNode}, this will be the tag's + * text. + * - In the case of a {@link Autolinker.htmlParser.TextNode TextNode}, this will be the text itself. + * - In the case of a {@link Autolinker.htmlParser.EntityNode EntityNode}, this will be the text of + * the HTML entity. + */ + text : "", + + + /** + * @constructor + * @param {Object} cfg The configuration properties for the Match instance, specified in an Object (map). + */ + constructor : function( cfg ) { + Autolinker.Util.assign( this, cfg ); + }, + + + /** + * Returns a string name for the type of node that this class represents. + * + * @abstract + * @return {String} + */ + getType : Autolinker.Util.abstractMethod, + + + /** + * Retrieves the {@link #text} for the HtmlNode. + * + * @return {String} + */ + getText : function() { + return this.text; + } + +} ); +/*global Autolinker */ +/** + * @class Autolinker.htmlParser.CommentNode + * @extends Autolinker.htmlParser.HtmlNode + * + * Represents an HTML comment node that has been parsed by the + * {@link Autolinker.htmlParser.HtmlParser}. + * + * See this class's superclass ({@link Autolinker.htmlParser.HtmlNode}) for more + * details. + */ +Autolinker.htmlParser.CommentNode = Autolinker.Util.extend( Autolinker.htmlParser.HtmlNode, { + + /** + * @cfg {String} comment (required) + * + * The text inside the comment tag. This text is stripped of any leading or + * trailing whitespace. + */ + comment : '', + + + /** + * Returns a string name for the type of node that this class represents. + * + * @return {String} + */ + getType : function() { + return 'comment'; + }, + + + /** + * Returns the comment inside the comment tag. + * + * @return {String} + */ + getComment : function() { + return this.comment; + } + +} ); +/*global Autolinker */ +/** + * @class Autolinker.htmlParser.ElementNode + * @extends Autolinker.htmlParser.HtmlNode + * + * Represents an HTML element node that has been parsed by the {@link Autolinker.htmlParser.HtmlParser}. + * + * See this class's superclass ({@link Autolinker.htmlParser.HtmlNode}) for more details. + */ +Autolinker.htmlParser.ElementNode = Autolinker.Util.extend( Autolinker.htmlParser.HtmlNode, { + + /** + * @cfg {String} tagName (required) + * + * The name of the tag that was matched. + */ + tagName : '', + + /** + * @cfg {Boolean} closing (required) + * + * `true` if the element (tag) is a closing tag, `false` if its an opening tag. + */ + closing : false, + + + /** + * Returns a string name for the type of node that this class represents. + * + * @return {String} + */ + getType : function() { + return 'element'; + }, + + + /** + * Returns the HTML element's (tag's) name. Ex: for an <img> tag, returns "img". + * + * @return {String} + */ + getTagName : function() { + return this.tagName; + }, + + + /** + * Determines if the HTML element (tag) is a closing tag. Ex: <div> returns + * `false`, while </div> returns `true`. + * + * @return {Boolean} + */ + isClosing : function() { + return this.closing; + } + +} ); +/*global Autolinker */ +/** + * @class Autolinker.htmlParser.EntityNode + * @extends Autolinker.htmlParser.HtmlNode + * + * Represents a known HTML entity node that has been parsed by the {@link Autolinker.htmlParser.HtmlParser}. + * Ex: '&nbsp;', or '&#160;' (which will be retrievable from the {@link #getText} method. + * + * Note that this class will only be returned from the HtmlParser for the set of checked HTML entity nodes + * defined by the {@link Autolinker.htmlParser.HtmlParser#htmlCharacterEntitiesRegex}. + * + * See this class's superclass ({@link Autolinker.htmlParser.HtmlNode}) for more details. + */ +Autolinker.htmlParser.EntityNode = Autolinker.Util.extend( Autolinker.htmlParser.HtmlNode, { + + /** + * Returns a string name for the type of node that this class represents. + * + * @return {String} + */ + getType : function() { + return 'entity'; + } + +} ); +/*global Autolinker */ +/** + * @class Autolinker.htmlParser.TextNode + * @extends Autolinker.htmlParser.HtmlNode + * + * Represents a text node that has been parsed by the {@link Autolinker.htmlParser.HtmlParser}. + * + * See this class's superclass ({@link Autolinker.htmlParser.HtmlNode}) for more details. + */ +Autolinker.htmlParser.TextNode = Autolinker.Util.extend( Autolinker.htmlParser.HtmlNode, { + + /** + * Returns a string name for the type of node that this class represents. + * + * @return {String} + */ + getType : function() { + return 'text'; + } + +} ); +/*global Autolinker */ +/** + * @private + * @class Autolinker.matchParser.MatchParser + * @extends Object + * + * Used by Autolinker to parse potential matches, given an input string of text. + * + * The MatchParser is fed a non-HTML string in order to search for matches. + * Autolinker first uses the {@link Autolinker.htmlParser.HtmlParser} to "walk + * around" HTML tags, and then the text around the HTML tags is passed into the + * MatchParser in order to find the actual matches. + */ +Autolinker.matchParser.MatchParser = Autolinker.Util.extend( Object, { + + /** + * @cfg {Boolean} urls + * @inheritdoc Autolinker#urls + */ + urls : true, + + /** + * @cfg {Boolean} email + * @inheritdoc Autolinker#email + */ + email : true, + + /** + * @cfg {Boolean} twitter + * @inheritdoc Autolinker#twitter + */ + twitter : true, + + /** + * @cfg {Boolean} phone + * @inheritdoc Autolinker#phone + */ + phone: true, + + /** + * @cfg {Boolean/String} hashtag + * @inheritdoc Autolinker#hashtag + */ + hashtag : false, + + /** + * @cfg {Boolean} stripPrefix + * @inheritdoc Autolinker#stripPrefix + */ + stripPrefix : true, + + + /** + * @private + * @property {RegExp} matcherRegex + * + * The regular expression that matches URLs, email addresses, phone #s, + * Twitter handles, and Hashtags. + * + * This regular expression has the following capturing groups: + * + * 1. Group that is used to determine if there is a Twitter handle match + * (i.e. \@someTwitterUser). Simply check for its existence to determine + * if there is a Twitter handle match. The next couple of capturing + * groups give information about the Twitter handle match. + * 2. The whitespace character before the \@sign in a Twitter handle. This + * is needed because there are no lookbehinds in JS regular expressions, + * and can be used to reconstruct the original string in a replace(). + * 3. The Twitter handle itself in a Twitter match. If the match is + * '@someTwitterUser', the handle is 'someTwitterUser'. + * 4. Group that matches an email address. Used to determine if the match + * is an email address, as well as holding the full address. Ex: + * 'me@my.com' + * 5. Group that matches a URL in the input text. Ex: 'http://google.com', + * 'www.google.com', or just 'google.com'. This also includes a path, + * url parameters, or hash anchors. Ex: google.com/path/to/file?q1=1&q2=2#myAnchor + * 6. Group that matches a protocol URL (i.e. 'http://google.com'). This is + * used to match protocol URLs with just a single word, like 'http://localhost', + * where we won't double check that the domain name has at least one '.' + * in it. + * 7. A protocol-relative ('//') match for the case of a 'www.' prefixed + * URL. Will be an empty string if it is not a protocol-relative match. + * We need to know the character before the '//' in order to determine + * if it is a valid match or the // was in a string we don't want to + * auto-link. + * 8. A protocol-relative ('//') match for the case of a known TLD prefixed + * URL. Will be an empty string if it is not a protocol-relative match. + * See #6 for more info. + * 9. Group that is used to determine if there is a phone number match. The + * next 3 groups give segments of the phone number. + * 10. Group that is used to determine if there is a Hashtag match + * (i.e. \#someHashtag). Simply check for its existence to determine if + * there is a Hashtag match. The next couple of capturing groups give + * information about the Hashtag match. + * 11. The whitespace character before the #sign in a Hashtag handle. This + * is needed because there are no look-behinds in JS regular + * expressions, and can be used to reconstruct the original string in a + * replace(). + * 12. The Hashtag itself in a Hashtag match. If the match is + * '#someHashtag', the hashtag is 'someHashtag'. + */ + matcherRegex : (function() { + var twitterRegex = /(^|[^\w])@(\w{1,15})/, // For matching a twitter handle. Ex: @gregory_jacobs + + hashtagRegex = /(^|[^\w])#(\w{1,15})/, // For matching a Hashtag. Ex: #games + + emailRegex = /(?:[\-;:&=\+\$,\w\.]+@)/, // something@ for email addresses (a.k.a. local-part) + phoneRegex = /(?:\+?\d{1,3}[-\s.])?\(?\d{3}\)?[-\s.]?\d{3}[-\s.]\d{4}/, // ex: (123) 456-7890, 123 456 7890, 123-456-7890, etc. + protocolRegex = /(?:[A-Za-z][-.+A-Za-z0-9]+:(?![A-Za-z][-.+A-Za-z0-9]+:\/\/)(?!\d+\/?)(?:\/\/)?)/, // match protocol, allow in format "http://" or "mailto:". However, do not match the first part of something like 'link:http://www.google.com' (i.e. don't match "link:"). Also, make sure we don't interpret 'google.com:8000' as if 'google.com' was a protocol here (i.e. ignore a trailing port number in this regex) + wwwRegex = /(?:www\.)/, // starting with 'www.' + domainNameRegex = /[A-Za-z0-9\.\-]*[A-Za-z0-9\-]/, // anything looking at all like a domain, non-unicode domains, not ending in a period + tldRegex = /\.(?:international|construction|contractors|enterprises|photography|productions|foundation|immobilien|industries|management|properties|technology|christmas|community|directory|education|equipment|institute|marketing|solutions|vacations|bargains|boutique|builders|catering|cleaning|clothing|computer|democrat|diamonds|graphics|holdings|lighting|partners|plumbing|supplies|training|ventures|academy|careers|company|cruises|domains|exposed|flights|florist|gallery|guitars|holiday|kitchen|neustar|okinawa|recipes|rentals|reviews|shiksha|singles|support|systems|agency|berlin|camera|center|coffee|condos|dating|estate|events|expert|futbol|kaufen|luxury|maison|monash|museum|nagoya|photos|repair|report|social|supply|tattoo|tienda|travel|viajes|villas|vision|voting|voyage|actor|build|cards|cheap|codes|dance|email|glass|house|mango|ninja|parts|photo|shoes|solar|today|tokyo|tools|watch|works|aero|arpa|asia|best|bike|blue|buzz|camp|club|cool|coop|farm|fish|gift|guru|info|jobs|kiwi|kred|land|limo|link|menu|mobi|moda|name|pics|pink|post|qpon|rich|ruhr|sexy|tips|vote|voto|wang|wien|wiki|zone|bar|bid|biz|cab|cat|ceo|com|edu|gov|int|kim|mil|net|onl|org|pro|pub|red|tel|uno|wed|xxx|xyz|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)\b/, // match our known top level domains (TLDs) + + // Allow optional path, query string, and hash anchor, not ending in the following characters: "?!:,.;" + // http://blog.codinghorror.com/the-problem-with-urls/ + urlSuffixRegex = /[\-A-Za-z0-9+&@#\/%=~_()|'$*\[\]?!:,.;]*[\-A-Za-z0-9+&@#\/%=~_()|'$*\[\]]/; + + return new RegExp( [ + '(', // *** Capturing group $1, which can be used to check for a twitter handle match. Use group $3 for the actual twitter handle though. $2 may be used to reconstruct the original string in a replace() + // *** Capturing group $2, which matches the whitespace character before the '@' sign (needed because of no lookbehinds), and + // *** Capturing group $3, which matches the actual twitter handle + twitterRegex.source, + ')', + + '|', + + '(', // *** Capturing group $4, which is used to determine an email match + emailRegex.source, + domainNameRegex.source, + tldRegex.source, + ')', + + '|', + + '(', // *** Capturing group $5, which is used to match a URL + '(?:', // parens to cover match for protocol (optional), and domain + '(', // *** Capturing group $6, for a protocol-prefixed url (ex: http://google.com) + protocolRegex.source, + domainNameRegex.source, + ')', + + '|', + + '(?:', // non-capturing paren for a 'www.' prefixed url (ex: www.google.com) + '(.?//)?', // *** Capturing group $7 for an optional protocol-relative URL. Must be at the beginning of the string or start with a non-word character + wwwRegex.source, + domainNameRegex.source, + ')', + + '|', + + '(?:', // non-capturing paren for known a TLD url (ex: google.com) + '(.?//)?', // *** Capturing group $8 for an optional protocol-relative URL. Must be at the beginning of the string or start with a non-word character + domainNameRegex.source, + tldRegex.source, + ')', + ')', + + '(?:' + urlSuffixRegex.source + ')?', // match for path, query string, and/or hash anchor - optional + ')', + + '|', + + // this setup does not scale well for open extension :( Need to rethink design of autolinker... + // *** Capturing group $9, which matches a (USA for now) phone number + '(', + phoneRegex.source, + ')', + + '|', + + '(', // *** Capturing group $10, which can be used to check for a Hashtag match. Use group $12 for the actual Hashtag though. $11 may be used to reconstruct the original string in a replace() + // *** Capturing group $11, which matches the whitespace character before the '#' sign (needed because of no lookbehinds), and + // *** Capturing group $12, which matches the actual Hashtag + hashtagRegex.source, + ')' + ].join( "" ), 'gi' ); + } )(), + + /** + * @private + * @property {RegExp} charBeforeProtocolRelMatchRegex + * + * The regular expression used to retrieve the character before a + * protocol-relative URL match. + * + * This is used in conjunction with the {@link #matcherRegex}, which needs + * to grab the character before a protocol-relative '//' due to the lack of + * a negative look-behind in JavaScript regular expressions. The character + * before the match is stripped from the URL. + */ + charBeforeProtocolRelMatchRegex : /^(.)?\/\//, + + /** + * @private + * @property {Autolinker.MatchValidator} matchValidator + * + * The MatchValidator object, used to filter out any false positives from + * the {@link #matcherRegex}. See {@link Autolinker.MatchValidator} for details. + */ + + + /** + * @constructor + * @param {Object} [cfg] The configuration options for the AnchorTagBuilder + * instance, specified in an Object (map). + */ + constructor : function( cfg ) { + Autolinker.Util.assign( this, cfg ); + + this.matchValidator = new Autolinker.MatchValidator(); + }, + + + /** + * Parses the input `text` to search for matches, and calls the `replaceFn` + * to allow replacements of the matches. Returns the `text` with matches + * replaced. + * + * @param {String} text The text to search and repace matches in. + * @param {Function} replaceFn The iterator function to handle the + * replacements. The function takes a single argument, a {@link Autolinker.match.Match} + * object, and should return the text that should make the replacement. + * @param {Object} [contextObj=window] The context object ("scope") to run + * the `replaceFn` in. + * @return {String} + */ + replace : function( text, replaceFn, contextObj ) { + var me = this; // for closure + + return text.replace( this.matcherRegex, function( matchStr, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 ) { + var matchDescObj = me.processCandidateMatch( matchStr, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 ); // "match description" object + + // Return out with no changes for match types that are disabled (url, + // email, phone, etc.), or for matches that are invalid (false + // positives from the matcherRegex, which can't use look-behinds + // since they are unavailable in JS). + if( !matchDescObj ) { + return matchStr; + + } else { + // Generate replacement text for the match from the `replaceFn` + var replaceStr = replaceFn.call( contextObj, matchDescObj.match ); + return matchDescObj.prefixStr + replaceStr + matchDescObj.suffixStr; + } + } ); + }, + + + /** + * Processes a candidate match from the {@link #matcherRegex}. + * + * Not all matches found by the regex are actual URL/Email/Phone/Twitter/Hashtag + * matches, as determined by the {@link #matchValidator}. In this case, the + * method returns `null`. Otherwise, a valid Object with `prefixStr`, + * `match`, and `suffixStr` is returned. + * + * @private + * @param {String} matchStr The full match that was found by the + * {@link #matcherRegex}. + * @param {String} twitterMatch The matched text of a Twitter handle, if the + * match is a Twitter match. + * @param {String} twitterHandlePrefixWhitespaceChar The whitespace char + * before the @ sign in a Twitter handle match. This is needed because of + * no lookbehinds in JS regexes, and is need to re-include the character + * for the anchor tag replacement. + * @param {String} twitterHandle The actual Twitter user (i.e the word after + * the @ sign in a Twitter match). + * @param {String} emailAddressMatch The matched email address for an email + * address match. + * @param {String} urlMatch The matched URL string for a URL match. + * @param {String} protocolUrlMatch The match URL string for a protocol + * match. Ex: 'http://yahoo.com'. This is used to match something like + * 'http://localhost', where we won't double check that the domain name + * has at least one '.' in it. + * @param {String} wwwProtocolRelativeMatch The '//' for a protocol-relative + * match from a 'www' url, with the character that comes before the '//'. + * @param {String} tldProtocolRelativeMatch The '//' for a protocol-relative + * match from a TLD (top level domain) match, with the character that + * comes before the '//'. + * @param {String} phoneMatch The matched text of a phone number + * @param {String} hashtagMatch The matched text of a Twitter + * Hashtag, if the match is a Hashtag match. + * @param {String} hashtagPrefixWhitespaceChar The whitespace char + * before the # sign in a Hashtag match. This is needed because of no + * lookbehinds in JS regexes, and is need to re-include the character for + * the anchor tag replacement. + * @param {String} hashtag The actual Hashtag (i.e the word + * after the # sign in a Hashtag match). + * + * @return {Object} A "match description object". This will be `null` if the + * match was invalid, or if a match type is disabled. Otherwise, this will + * be an Object (map) with the following properties: + * @return {String} return.prefixStr The char(s) that should be prepended to + * the replacement string. These are char(s) that were needed to be + * included from the regex match that were ignored by processing code, and + * should be re-inserted into the replacement stream. + * @return {String} return.suffixStr The char(s) that should be appended to + * the replacement string. These are char(s) that were needed to be + * included from the regex match that were ignored by processing code, and + * should be re-inserted into the replacement stream. + * @return {Autolinker.match.Match} return.match The Match object that + * represents the match that was found. + */ + processCandidateMatch : function( + matchStr, twitterMatch, twitterHandlePrefixWhitespaceChar, twitterHandle, + emailAddressMatch, urlMatch, protocolUrlMatch, wwwProtocolRelativeMatch, + tldProtocolRelativeMatch, phoneMatch, hashtagMatch, + hashtagPrefixWhitespaceChar, hashtag + ) { + // Note: The `matchStr` variable wil be fixed up to remove characters that are no longer needed (which will + // be added to `prefixStr` and `suffixStr`). + + var protocolRelativeMatch = wwwProtocolRelativeMatch || tldProtocolRelativeMatch, + match, // Will be an Autolinker.match.Match object + + prefixStr = "", // A string to use to prefix the anchor tag that is created. This is needed for the Twitter and Hashtag matches. + suffixStr = ""; // A string to suffix the anchor tag that is created. This is used if there is a trailing parenthesis that should not be auto-linked. + + // Return out with `null` for match types that are disabled (url, email, + // twitter, hashtag), or for matches that are invalid (false positives + // from the matcherRegex, which can't use look-behinds since they are + // unavailable in JS). + if( + ( urlMatch && !this.urls ) || + ( emailAddressMatch && !this.email ) || + ( phoneMatch && !this.phone ) || + ( twitterMatch && !this.twitter ) || + ( hashtagMatch && !this.hashtag ) || + !this.matchValidator.isValidMatch( urlMatch, protocolUrlMatch, protocolRelativeMatch ) + ) { + return null; + } + + // Handle a closing parenthesis at the end of the match, and exclude it + // if there is not a matching open parenthesis + // in the match itself. + if( this.matchHasUnbalancedClosingParen( matchStr ) ) { + matchStr = matchStr.substr( 0, matchStr.length - 1 ); // remove the trailing ")" + suffixStr = ")"; // this will be added after the generated tag + } + + if( emailAddressMatch ) { + match = new Autolinker.match.Email( { matchedText: matchStr, email: emailAddressMatch } ); + + } else if( twitterMatch ) { + // fix up the `matchStr` if there was a preceding whitespace char, + // which was needed to determine the match itself (since there are + // no look-behinds in JS regexes) + if( twitterHandlePrefixWhitespaceChar ) { + prefixStr = twitterHandlePrefixWhitespaceChar; + matchStr = matchStr.slice( 1 ); // remove the prefixed whitespace char from the match + } + match = new Autolinker.match.Twitter( { matchedText: matchStr, twitterHandle: twitterHandle } ); + + } else if( phoneMatch ) { + // remove non-numeric values from phone number string + var cleanNumber = matchStr.replace( /\D/g, '' ); + match = new Autolinker.match.Phone( { matchedText: matchStr, number: cleanNumber } ); + + } else if( hashtagMatch ) { + // fix up the `matchStr` if there was a preceding whitespace char, + // which was needed to determine the match itself (since there are + // no look-behinds in JS regexes) + if( hashtagPrefixWhitespaceChar ) { + prefixStr = hashtagPrefixWhitespaceChar; + matchStr = matchStr.slice( 1 ); // remove the prefixed whitespace char from the match + } + match = new Autolinker.match.Hashtag( { matchedText: matchStr, serviceName: this.hashtag, hashtag: hashtag } ); + + } else { // url match + // If it's a protocol-relative '//' match, remove the character + // before the '//' (which the matcherRegex needed to match due to + // the lack of a negative look-behind in JavaScript regular + // expressions) + if( protocolRelativeMatch ) { + var charBeforeMatch = protocolRelativeMatch.match( this.charBeforeProtocolRelMatchRegex )[ 1 ] || ""; + + if( charBeforeMatch ) { // fix up the `matchStr` if there was a preceding char before a protocol-relative match, which was needed to determine the match itself (since there are no look-behinds in JS regexes) + prefixStr = charBeforeMatch; + matchStr = matchStr.slice( 1 ); // remove the prefixed char from the match + } + } + + match = new Autolinker.match.Url( { + matchedText : matchStr, + url : matchStr, + protocolUrlMatch : !!protocolUrlMatch, + protocolRelativeMatch : !!protocolRelativeMatch, + stripPrefix : this.stripPrefix + } ); + } + + return { + prefixStr : prefixStr, + suffixStr : suffixStr, + match : match + }; + }, + + + /** + * Determines if a match found has an unmatched closing parenthesis. If so, + * this parenthesis will be removed from the match itself, and appended + * after the generated anchor tag in {@link #processCandidateMatch}. + * + * A match may have an extra closing parenthesis at the end of the match + * because the regular expression must include parenthesis for URLs such as + * "wikipedia.com/something_(disambiguation)", which should be auto-linked. + * + * However, an extra parenthesis *will* be included when the URL itself is + * wrapped in parenthesis, such as in the case of "(wikipedia.com/something_(disambiguation))". + * In this case, the last closing parenthesis should *not* be part of the + * URL itself, and this method will return `true`. + * + * @private + * @param {String} matchStr The full match string from the {@link #matcherRegex}. + * @return {Boolean} `true` if there is an unbalanced closing parenthesis at + * the end of the `matchStr`, `false` otherwise. + */ + matchHasUnbalancedClosingParen : function( matchStr ) { + var lastChar = matchStr.charAt( matchStr.length - 1 ); + + if( lastChar === ')' ) { + var openParensMatch = matchStr.match( /\(/g ), + closeParensMatch = matchStr.match( /\)/g ), + numOpenParens = ( openParensMatch && openParensMatch.length ) || 0, + numCloseParens = ( closeParensMatch && closeParensMatch.length ) || 0; + + if( numOpenParens < numCloseParens ) { + return true; + } + } + + return false; + } + +} ); +/*global Autolinker */ +/*jshint scripturl:true */ +/** + * @private + * @class Autolinker.MatchValidator + * @extends Object + * + * Used by Autolinker to filter out false positives from the + * {@link Autolinker.matchParser.MatchParser#matcherRegex}. + * + * Due to the limitations of regular expressions (including the missing feature + * of look-behinds in JS regular expressions), we cannot always determine the + * validity of a given match. This class applies a bit of additional logic to + * filter out any false positives that have been matched by the + * {@link Autolinker.matchParser.MatchParser#matcherRegex}. + */ +Autolinker.MatchValidator = Autolinker.Util.extend( Object, { + + /** + * @private + * @property {RegExp} invalidProtocolRelMatchRegex + * + * The regular expression used to check a potential protocol-relative URL + * match, coming from the {@link Autolinker.matchParser.MatchParser#matcherRegex}. + * A protocol-relative URL is, for example, "//yahoo.com" + * + * This regular expression checks to see if there is a word character before + * the '//' match in order to determine if we should actually autolink a + * protocol-relative URL. This is needed because there is no negative + * look-behind in JavaScript regular expressions. + * + * For instance, we want to autolink something like "Go to: //google.com", + * but we don't want to autolink something like "abc//google.com" + */ + invalidProtocolRelMatchRegex : /^[\w]\/\//, + + /** + * Regex to test for a full protocol, with the two trailing slashes. Ex: 'http://' + * + * @private + * @property {RegExp} hasFullProtocolRegex + */ + hasFullProtocolRegex : /^[A-Za-z][-.+A-Za-z0-9]+:\/\//, + + /** + * Regex to find the URI scheme, such as 'mailto:'. + * + * This is used to filter out 'javascript:' and 'vbscript:' schemes. + * + * @private + * @property {RegExp} uriSchemeRegex + */ + uriSchemeRegex : /^[A-Za-z][-.+A-Za-z0-9]+:/, + + /** + * Regex to determine if at least one word char exists after the protocol (i.e. after the ':') + * + * @private + * @property {RegExp} hasWordCharAfterProtocolRegex + */ + hasWordCharAfterProtocolRegex : /:[^\s]*?[A-Za-z]/, + + + /** + * Determines if a given match found by the {@link Autolinker.matchParser.MatchParser} + * is valid. Will return `false` for: + * + * 1) URL matches which do not have at least have one period ('.') in the + * domain name (effectively skipping over matches like "abc:def"). + * However, URL matches with a protocol will be allowed (ex: 'http://localhost') + * 2) URL matches which do not have at least one word character in the + * domain name (effectively skipping over matches like "git:1.0"). + * 3) A protocol-relative url match (a URL beginning with '//') whose + * previous character is a word character (effectively skipping over + * strings like "abc//google.com") + * + * Otherwise, returns `true`. + * + * @param {String} urlMatch The matched URL, if there was one. Will be an + * empty string if the match is not a URL match. + * @param {String} protocolUrlMatch The match URL string for a protocol + * match. Ex: 'http://yahoo.com'. This is used to match something like + * 'http://localhost', where we won't double check that the domain name + * has at least one '.' in it. + * @param {String} protocolRelativeMatch The protocol-relative string for a + * URL match (i.e. '//'), possibly with a preceding character (ex, a + * space, such as: ' //', or a letter, such as: 'a//'). The match is + * invalid if there is a word character preceding the '//'. + * @return {Boolean} `true` if the match given is valid and should be + * processed, or `false` if the match is invalid and/or should just not be + * processed. + */ + isValidMatch : function( urlMatch, protocolUrlMatch, protocolRelativeMatch ) { + if( + ( protocolUrlMatch && !this.isValidUriScheme( protocolUrlMatch ) ) || + this.urlMatchDoesNotHaveProtocolOrDot( urlMatch, protocolUrlMatch ) || // At least one period ('.') must exist in the URL match for us to consider it an actual URL, *unless* it was a full protocol match (like 'http://localhost') + this.urlMatchDoesNotHaveAtLeastOneWordChar( urlMatch, protocolUrlMatch ) || // At least one letter character must exist in the domain name after a protocol match. Ex: skip over something like "git:1.0" + this.isInvalidProtocolRelativeMatch( protocolRelativeMatch ) // A protocol-relative match which has a word character in front of it (so we can skip something like "abc//google.com") + ) { + return false; + } + + return true; + }, + + + /** + * Determines if the URI scheme is a valid scheme to be autolinked. Returns + * `false` if the scheme is 'javascript:' or 'vbscript:' + * + * @private + * @param {String} uriSchemeMatch The match URL string for a full URI scheme + * match. Ex: 'http://yahoo.com' or 'mailto:a@a.com'. + * @return {Boolean} `true` if the scheme is a valid one, `false` otherwise. + */ + isValidUriScheme : function( uriSchemeMatch ) { + var uriScheme = uriSchemeMatch.match( this.uriSchemeRegex )[ 0 ].toLowerCase(); + + return ( uriScheme !== 'javascript:' && uriScheme !== 'vbscript:' ); + }, + + + /** + * Determines if a URL match does not have either: + * + * a) a full protocol (i.e. 'http://'), or + * b) at least one dot ('.') in the domain name (for a non-full-protocol + * match). + * + * Either situation is considered an invalid URL (ex: 'git:d' does not have + * either the '://' part, or at least one dot in the domain name. If the + * match was 'git:abc.com', we would consider this valid.) + * + * @private + * @param {String} urlMatch The matched URL, if there was one. Will be an + * empty string if the match is not a URL match. + * @param {String} protocolUrlMatch The match URL string for a protocol + * match. Ex: 'http://yahoo.com'. This is used to match something like + * 'http://localhost', where we won't double check that the domain name + * has at least one '.' in it. + * @return {Boolean} `true` if the URL match does not have a full protocol, + * or at least one dot ('.') in a non-full-protocol match. + */ + urlMatchDoesNotHaveProtocolOrDot : function( urlMatch, protocolUrlMatch ) { + return ( !!urlMatch && ( !protocolUrlMatch || !this.hasFullProtocolRegex.test( protocolUrlMatch ) ) && urlMatch.indexOf( '.' ) === -1 ); + }, + + + /** + * Determines if a URL match does not have at least one word character after + * the protocol (i.e. in the domain name). + * + * At least one letter character must exist in the domain name after a + * protocol match. Ex: skip over something like "git:1.0" + * + * @private + * @param {String} urlMatch The matched URL, if there was one. Will be an + * empty string if the match is not a URL match. + * @param {String} protocolUrlMatch The match URL string for a protocol + * match. Ex: 'http://yahoo.com'. This is used to know whether or not we + * have a protocol in the URL string, in order to check for a word + * character after the protocol separator (':'). + * @return {Boolean} `true` if the URL match does not have at least one word + * character in it after the protocol, `false` otherwise. + */ + urlMatchDoesNotHaveAtLeastOneWordChar : function( urlMatch, protocolUrlMatch ) { + if( urlMatch && protocolUrlMatch ) { + return !this.hasWordCharAfterProtocolRegex.test( urlMatch ); + } else { + return false; + } + }, + + + /** + * Determines if a protocol-relative match is an invalid one. This method + * returns `true` if there is a `protocolRelativeMatch`, and that match + * contains a word character before the '//' (i.e. it must contain + * whitespace or nothing before the '//' in order to be considered valid). + * + * @private + * @param {String} protocolRelativeMatch The protocol-relative string for a + * URL match (i.e. '//'), possibly with a preceding character (ex, a + * space, such as: ' //', or a letter, such as: 'a//'). The match is + * invalid if there is a word character preceding the '//'. + * @return {Boolean} `true` if it is an invalid protocol-relative match, + * `false` otherwise. + */ + isInvalidProtocolRelativeMatch : function( protocolRelativeMatch ) { + return ( !!protocolRelativeMatch && this.invalidProtocolRelMatchRegex.test( protocolRelativeMatch ) ); + } + +} ); +/*global Autolinker */ +/** + * @abstract + * @class Autolinker.match.Match + * + * Represents a match found in an input string which should be Autolinked. A Match object is what is provided in a + * {@link Autolinker#replaceFn replaceFn}, and may be used to query for details about the match. + * + * For example: + * + * var input = "..."; // string with URLs, Email Addresses, and Twitter Handles + * + * var linkedText = Autolinker.link( input, { + * replaceFn : function( autolinker, match ) { + * console.log( "href = ", match.getAnchorHref() ); + * console.log( "text = ", match.getAnchorText() ); + * + * switch( match.getType() ) { + * case 'url' : + * console.log( "url: ", match.getUrl() ); + * + * case 'email' : + * console.log( "email: ", match.getEmail() ); + * + * case 'twitter' : + * console.log( "twitter: ", match.getTwitterHandle() ); + * } + * } + * } ); + * + * See the {@link Autolinker} class for more details on using the {@link Autolinker#replaceFn replaceFn}. + */ +Autolinker.match.Match = Autolinker.Util.extend( Object, { + + /** + * @cfg {String} matchedText (required) + * + * The original text that was matched. + */ + + + /** + * @constructor + * @param {Object} cfg The configuration properties for the Match instance, specified in an Object (map). + */ + constructor : function( cfg ) { + Autolinker.Util.assign( this, cfg ); + }, + + + /** + * Returns a string name for the type of match that this class represents. + * + * @abstract + * @return {String} + */ + getType : Autolinker.Util.abstractMethod, + + + /** + * Returns the original text that was matched. + * + * @return {String} + */ + getMatchedText : function() { + return this.matchedText; + }, + + + /** + * Returns the anchor href that should be generated for the match. + * + * @abstract + * @return {String} + */ + getAnchorHref : Autolinker.Util.abstractMethod, + + + /** + * Returns the anchor text that should be generated for the match. + * + * @abstract + * @return {String} + */ + getAnchorText : Autolinker.Util.abstractMethod + +} ); +/*global Autolinker */ +/** + * @class Autolinker.match.Email + * @extends Autolinker.match.Match + * + * Represents a Email match found in an input string which should be Autolinked. + * + * See this class's superclass ({@link Autolinker.match.Match}) for more details. + */ +Autolinker.match.Email = Autolinker.Util.extend( Autolinker.match.Match, { + + /** + * @cfg {String} email (required) + * + * The email address that was matched. + */ + + + /** + * Returns a string name for the type of match that this class represents. + * + * @return {String} + */ + getType : function() { + return 'email'; + }, + + + /** + * Returns the email address that was matched. + * + * @return {String} + */ + getEmail : function() { + return this.email; + }, + + + /** + * Returns the anchor href that should be generated for the match. + * + * @return {String} + */ + getAnchorHref : function() { + return 'mailto:' + this.email; + }, + + + /** + * Returns the anchor text that should be generated for the match. + * + * @return {String} + */ + getAnchorText : function() { + return this.email; + } + +} ); +/*global Autolinker */ +/** + * @class Autolinker.match.Hashtag + * @extends Autolinker.match.Match + * + * Represents a Hashtag match found in an input string which should be + * Autolinked. + * + * See this class's superclass ({@link Autolinker.match.Match}) for more + * details. + */ +Autolinker.match.Hashtag = Autolinker.Util.extend( Autolinker.match.Match, { + + /** + * @cfg {String} serviceName (required) + * + * The service to point hashtag matches to. See {@link Autolinker#hashtag} + * for available values. + */ + + /** + * @cfg {String} hashtag (required) + * + * The Hashtag that was matched, without the '#'. + */ + + + /** + * Returns the type of match that this class represents. + * + * @return {String} + */ + getType : function() { + return 'hashtag'; + }, + + + /** + * Returns the matched hashtag. + * + * @return {String} + */ + getHashtag : function() { + return this.hashtag; + }, + + + /** + * Returns the anchor href that should be generated for the match. + * + * @return {String} + */ + getAnchorHref : function() { + var serviceName = this.serviceName, + hashtag = this.hashtag; + + switch( serviceName ) { + case 'twitter' : + return 'https://twitter.com/hashtag/' + hashtag; + case 'facebook' : + return 'https://www.facebook.com/hashtag/' + hashtag; + + default : // Shouldn't happen because Autolinker's constructor should block any invalid values, but just in case. + throw new Error( 'Unknown service name to point hashtag to: ', serviceName ); + } + }, + + + /** + * Returns the anchor text that should be generated for the match. + * + * @return {String} + */ + getAnchorText : function() { + return '#' + this.hashtag; + } + +} ); +/*global Autolinker */ +/** + * @class Autolinker.match.Phone + * @extends Autolinker.match.Match + * + * Represents a Phone number match found in an input string which should be + * Autolinked. + * + * See this class's superclass ({@link Autolinker.match.Match}) for more + * details. + */ +Autolinker.match.Phone = Autolinker.Util.extend( Autolinker.match.Match, { + + /** + * @cfg {String} number (required) + * + * The phone number that was matched. + */ + + + /** + * Returns a string name for the type of match that this class represents. + * + * @return {String} + */ + getType : function() { + return 'phone'; + }, + + + /** + * Returns the phone number that was matched. + * + * @return {String} + */ + getNumber: function() { + return this.number; + }, + + + /** + * Returns the anchor href that should be generated for the match. + * + * @return {String} + */ + getAnchorHref : function() { + return 'tel:' + this.number; + }, + + + /** + * Returns the anchor text that should be generated for the match. + * + * @return {String} + */ + getAnchorText : function() { + return this.matchedText; + } + +} ); + +/*global Autolinker */ +/** + * @class Autolinker.match.Twitter + * @extends Autolinker.match.Match + * + * Represents a Twitter match found in an input string which should be Autolinked. + * + * See this class's superclass ({@link Autolinker.match.Match}) for more details. + */ +Autolinker.match.Twitter = Autolinker.Util.extend( Autolinker.match.Match, { + + /** + * @cfg {String} twitterHandle (required) + * + * The Twitter handle that was matched. + */ + + + /** + * Returns the type of match that this class represents. + * + * @return {String} + */ + getType : function() { + return 'twitter'; + }, + + + /** + * Returns a string name for the type of match that this class represents. + * + * @return {String} + */ + getTwitterHandle : function() { + return this.twitterHandle; + }, + + + /** + * Returns the anchor href that should be generated for the match. + * + * @return {String} + */ + getAnchorHref : function() { + return 'https://twitter.com/' + this.twitterHandle; + }, + + + /** + * Returns the anchor text that should be generated for the match. + * + * @return {String} + */ + getAnchorText : function() { + return '@' + this.twitterHandle; + } + +} ); +/*global Autolinker */ +/** + * @class Autolinker.match.Url + * @extends Autolinker.match.Match + * + * Represents a Url match found in an input string which should be Autolinked. + * + * See this class's superclass ({@link Autolinker.match.Match}) for more details. + */ +Autolinker.match.Url = Autolinker.Util.extend( Autolinker.match.Match, { + + /** + * @cfg {String} url (required) + * + * The url that was matched. + */ + + /** + * @cfg {Boolean} protocolUrlMatch (required) + * + * `true` if the URL is a match which already has a protocol (i.e. 'http://'), `false` if the match was from a 'www' or + * known TLD match. + */ + + /** + * @cfg {Boolean} protocolRelativeMatch (required) + * + * `true` if the URL is a protocol-relative match. A protocol-relative match is a URL that starts with '//', + * and will be either http:// or https:// based on the protocol that the site is loaded under. + */ + + /** + * @cfg {Boolean} stripPrefix (required) + * @inheritdoc Autolinker#stripPrefix + */ + + + /** + * @private + * @property {RegExp} urlPrefixRegex + * + * A regular expression used to remove the 'http://' or 'https://' and/or the 'www.' from URLs. + */ + urlPrefixRegex: /^(https?:\/\/)?(www\.)?/i, + + /** + * @private + * @property {RegExp} protocolRelativeRegex + * + * The regular expression used to remove the protocol-relative '//' from the {@link #url} string, for purposes + * of {@link #getAnchorText}. A protocol-relative URL is, for example, "//yahoo.com" + */ + protocolRelativeRegex : /^\/\//, + + /** + * @private + * @property {Boolean} protocolPrepended + * + * Will be set to `true` if the 'http://' protocol has been prepended to the {@link #url} (because the + * {@link #url} did not have a protocol) + */ + protocolPrepended : false, + + + /** + * Returns a string name for the type of match that this class represents. + * + * @return {String} + */ + getType : function() { + return 'url'; + }, + + + /** + * Returns the url that was matched, assuming the protocol to be 'http://' if the original + * match was missing a protocol. + * + * @return {String} + */ + getUrl : function() { + var url = this.url; + + // if the url string doesn't begin with a protocol, assume 'http://' + if( !this.protocolRelativeMatch && !this.protocolUrlMatch && !this.protocolPrepended ) { + url = this.url = 'http://' + url; + + this.protocolPrepended = true; + } + + return url; + }, + + + /** + * Returns the anchor href that should be generated for the match. + * + * @return {String} + */ + getAnchorHref : function() { + var url = this.getUrl(); + + return url.replace( /&/g, '&' ); // any &'s in the URL should be converted back to '&' if they were displayed as & in the source html + }, + + + /** + * Returns the anchor text that should be generated for the match. + * + * @return {String} + */ + getAnchorText : function() { + var anchorText = this.getUrl(); + + if( this.protocolRelativeMatch ) { + // Strip off any protocol-relative '//' from the anchor text + anchorText = this.stripProtocolRelativePrefix( anchorText ); + } + if( this.stripPrefix ) { + anchorText = this.stripUrlPrefix( anchorText ); + } + anchorText = this.removeTrailingSlash( anchorText ); // remove trailing slash, if there is one + + return anchorText; + }, + + + // --------------------------------------- + + // Utility Functionality + + /** + * Strips the URL prefix (such as "http://" or "https://") from the given text. + * + * @private + * @param {String} text The text of the anchor that is being generated, for which to strip off the + * url prefix (such as stripping off "http://") + * @return {String} The `anchorText`, with the prefix stripped. + */ + stripUrlPrefix : function( text ) { + return text.replace( this.urlPrefixRegex, '' ); + }, + + + /** + * Strips any protocol-relative '//' from the anchor text. + * + * @private + * @param {String} text The text of the anchor that is being generated, for which to strip off the + * protocol-relative prefix (such as stripping off "//") + * @return {String} The `anchorText`, with the protocol-relative prefix stripped. + */ + stripProtocolRelativePrefix : function( text ) { + return text.replace( this.protocolRelativeRegex, '' ); + }, + + + /** + * Removes any trailing slash from the given `anchorText`, in preparation for the text to be displayed. + * + * @private + * @param {String} anchorText The text of the anchor that is being generated, for which to remove any trailing + * slash ('/') that may exist. + * @return {String} The `anchorText`, with the trailing slash removed. + */ + removeTrailingSlash : function( anchorText ) { + if( anchorText.charAt( anchorText.length - 1 ) === '/' ) { + anchorText = anchorText.slice( 0, -1 ); + } + return anchorText; + } + +} ); +return Autolinker; + +})); diff --git a/app/assets/javascripts/lib/attachMediaStream.js b/app/assets/javascripts/lib/attachMediaStream.js new file mode 100644 index 00000000..de3feef5 --- /dev/null +++ b/app/assets/javascripts/lib/attachMediaStream.js @@ -0,0 +1,39 @@ +var attachMediaStream = function (stream, el, options) { + var URL = window.URL; + var opts = { + autoplay: true, + mirror: false, + muted: false + }; + var element = el || document.createElement('video'); + var item; + + if (options) { + for (item in options) { + opts[item] = options[item]; + } + } + + if (opts.autoplay) element.autoplay = 'autoplay'; + if (opts.muted) element.muted = true; + if (opts.mirror) { + ['', 'moz', 'webkit', 'o', 'ms'].forEach(function (prefix) { + var styleName = prefix ? prefix + 'Transform' : 'transform'; + element.style[styleName] = 'scaleX(-1)'; + }); + } + + // this first one should work most everywhere now + // but we have a few fallbacks just in case. + if (URL && URL.createObjectURL) { + element.src = URL.createObjectURL(stream); + } else if (element.srcObject) { + element.srcObject = stream; + } else if (element.mozSrcObject) { + element.mozSrcObject = stream; + } else { + return false; + } + + return element; + }; \ No newline at end of file diff --git a/app/assets/javascripts/lib/howler.js b/app/assets/javascripts/lib/howler.js new file mode 100644 index 00000000..f393b3b1 --- /dev/null +++ b/app/assets/javascripts/lib/howler.js @@ -0,0 +1,1353 @@ +/*! + * howler.js v1.1.26 + * howlerjs.com + * + * (c) 2013-2015, James Simpson of GoldFire Studios + * goldfirestudios.com + * + * MIT License + */ + +(function() { + // setup + var cache = {}; + + // setup the audio context + var ctx = null, + usingWebAudio = true, + noAudio = false; + try { + if (typeof AudioContext !== 'undefined') { + ctx = new AudioContext(); + } else if (typeof webkitAudioContext !== 'undefined') { + ctx = new webkitAudioContext(); + } else { + usingWebAudio = false; + } + } catch(e) { + usingWebAudio = false; + } + + if (!usingWebAudio) { + if (typeof Audio !== 'undefined') { + try { + new Audio(); + } catch(e) { + noAudio = true; + } + } else { + noAudio = true; + } + } + + // create a master gain node + if (usingWebAudio) { + var masterGain = (typeof ctx.createGain === 'undefined') ? ctx.createGainNode() : ctx.createGain(); + masterGain.gain.value = 1; + masterGain.connect(ctx.destination); + } + + // create global controller + var HowlerGlobal = function(codecs) { + this._volume = 1; + this._muted = false; + this.usingWebAudio = usingWebAudio; + this.ctx = ctx; + this.noAudio = noAudio; + this._howls = []; + this._codecs = codecs; + this.iOSAutoEnable = true; + }; + HowlerGlobal.prototype = { + /** + * Get/set the global volume for all sounds. + * @param {Float} vol Volume from 0.0 to 1.0. + * @return {Howler/Float} Returns self or current volume. + */ + volume: function(vol) { + var self = this; + + // make sure volume is a number + vol = parseFloat(vol); + + if (vol >= 0 && vol <= 1) { + self._volume = vol; + + if (usingWebAudio) { + masterGain.gain.value = vol; + } + + // loop through cache and change volume of all nodes that are using HTML5 Audio + for (var key in self._howls) { + if (self._howls.hasOwnProperty(key) && self._howls[key]._webAudio === false) { + // loop through the audio nodes + for (var i=0; i 0) ? node._pos : self._sprite[sprite][0] / 1000; + + // determine how long to play for + var duration = 0; + if (self._webAudio) { + duration = self._sprite[sprite][1] / 1000 - node._pos; + if (node._pos > 0) { + pos = self._sprite[sprite][0] / 1000 + pos; + } + } else { + duration = self._sprite[sprite][1] / 1000 - (pos - self._sprite[sprite][0] / 1000); + } + + // determine if this sound should be looped + var loop = !!(self._loop || self._sprite[sprite][2]); + + // set timer to fire the 'onend' event + var soundId = (typeof callback === 'string') ? callback : Math.round(Date.now() * Math.random()) + '', + timerId; + (function() { + var data = { + id: soundId, + sprite: sprite, + loop: loop + }; + timerId = setTimeout(function() { + // if looping, restart the track + if (!self._webAudio && loop) { + self.stop(data.id).play(sprite, data.id); + } + + // set web audio node to paused at end + if (self._webAudio && !loop) { + self._nodeById(data.id).paused = true; + self._nodeById(data.id)._pos = 0; + + // clear the end timer + self._clearEndTimer(data.id); + } + + // end the track if it is HTML audio and a sprite + if (!self._webAudio && !loop) { + self.stop(data.id); + } + + // fire ended event + self.on('end', soundId); + }, duration * 1000); + + // store the reference to the timer + self._onendTimer.push({timer: timerId, id: data.id}); + })(); + + if (self._webAudio) { + var loopStart = self._sprite[sprite][0] / 1000, + loopEnd = self._sprite[sprite][1] / 1000; + + // set the play id to this node and load into context + node.id = soundId; + node.paused = false; + refreshBuffer(self, [loop, loopStart, loopEnd], soundId); + self._playStart = ctx.currentTime; + node.gain.value = self._volume; + + if (typeof node.bufferSource.start === 'undefined') { + loop ? node.bufferSource.noteGrainOn(0, pos, 86400) : node.bufferSource.noteGrainOn(0, pos, duration); + } else { + loop ? node.bufferSource.start(0, pos, 86400) : node.bufferSource.start(0, pos, duration); + } + } else { + if (node.readyState === 4 || !node.readyState && navigator.isCocoonJS) { + node.readyState = 4; + node.id = soundId; + node.currentTime = pos; + node.muted = Howler._muted || node.muted; + node.volume = self._volume * Howler.volume(); + setTimeout(function() { node.play(); }, 0); + } else { + self._clearEndTimer(soundId); + + (function(){ + var sound = self, + playSprite = sprite, + fn = callback, + newNode = node; + var listener = function() { + sound.play(playSprite, fn); + + // clear the event listener + newNode.removeEventListener('canplaythrough', listener, false); + }; + newNode.addEventListener('canplaythrough', listener, false); + })(); + + return self; + } + } + + // fire the play event and send the soundId back in the callback + self.on('play'); + if (typeof callback === 'function') callback(soundId); + + return self; + }); + + return self; + }, + + /** + * Pause playback and save the current position. + * @param {String} id (optional) The play instance ID. + * @return {Howl} + */ + pause: function(id) { + var self = this; + + // if the sound hasn't been loaded, add it to the event queue + if (!self._loaded) { + self.on('play', function() { + self.pause(id); + }); + + return self; + } + + // clear 'onend' timer + self._clearEndTimer(id); + + var activeNode = (id) ? self._nodeById(id) : self._activeNode(); + if (activeNode) { + activeNode._pos = self.pos(null, id); + + if (self._webAudio) { + // make sure the sound has been created + if (!activeNode.bufferSource || activeNode.paused) { + return self; + } + + activeNode.paused = true; + if (typeof activeNode.bufferSource.stop === 'undefined') { + activeNode.bufferSource.noteOff(0); + } else { + activeNode.bufferSource.stop(0); + } + } else { + activeNode.pause(); + } + } + + self.on('pause'); + + return self; + }, + + /** + * Stop playback and reset to start. + * @param {String} id (optional) The play instance ID. + * @return {Howl} + */ + stop: function(id) { + var self = this; + + // if the sound hasn't been loaded, add it to the event queue + if (!self._loaded) { + self.on('play', function() { + self.stop(id); + }); + + return self; + } + + // clear 'onend' timer + self._clearEndTimer(id); + + var activeNode = (id) ? self._nodeById(id) : self._activeNode(); + if (activeNode) { + activeNode._pos = 0; + + if (self._webAudio) { + // make sure the sound has been created + if (!activeNode.bufferSource || activeNode.paused) { + return self; + } + + activeNode.paused = true; + + if (typeof activeNode.bufferSource.stop === 'undefined') { + activeNode.bufferSource.noteOff(0); + } else { + activeNode.bufferSource.stop(0); + } + } else if (!isNaN(activeNode.duration)) { + activeNode.pause(); + activeNode.currentTime = 0; + } + } + + return self; + }, + + /** + * Mute this sound. + * @param {String} id (optional) The play instance ID. + * @return {Howl} + */ + mute: function(id) { + var self = this; + + // if the sound hasn't been loaded, add it to the event queue + if (!self._loaded) { + self.on('play', function() { + self.mute(id); + }); + + return self; + } + + var activeNode = (id) ? self._nodeById(id) : self._activeNode(); + if (activeNode) { + if (self._webAudio) { + activeNode.gain.value = 0; + } else { + activeNode.muted = true; + } + } + + return self; + }, + + /** + * Unmute this sound. + * @param {String} id (optional) The play instance ID. + * @return {Howl} + */ + unmute: function(id) { + var self = this; + + // if the sound hasn't been loaded, add it to the event queue + if (!self._loaded) { + self.on('play', function() { + self.unmute(id); + }); + + return self; + } + + var activeNode = (id) ? self._nodeById(id) : self._activeNode(); + if (activeNode) { + if (self._webAudio) { + activeNode.gain.value = self._volume; + } else { + activeNode.muted = false; + } + } + + return self; + }, + + /** + * Get/set volume of this sound. + * @param {Float} vol Volume from 0.0 to 1.0. + * @param {String} id (optional) The play instance ID. + * @return {Howl/Float} Returns self or current volume. + */ + volume: function(vol, id) { + var self = this; + + // make sure volume is a number + vol = parseFloat(vol); + + if (vol >= 0 && vol <= 1) { + self._volume = vol; + + // if the sound hasn't been loaded, add it to the event queue + if (!self._loaded) { + self.on('play', function() { + self.volume(vol, id); + }); + + return self; + } + + var activeNode = (id) ? self._nodeById(id) : self._activeNode(); + if (activeNode) { + if (self._webAudio) { + activeNode.gain.value = vol; + } else { + activeNode.volume = vol * Howler.volume(); + } + } + + return self; + } else { + return self._volume; + } + }, + + /** + * Get/set whether to loop the sound. + * @param {Boolean} loop To loop or not to loop, that is the question. + * @return {Howl/Boolean} Returns self or current looping value. + */ + loop: function(loop) { + var self = this; + + if (typeof loop === 'boolean') { + self._loop = loop; + + return self; + } else { + return self._loop; + } + }, + + /** + * Get/set sound sprite definition. + * @param {Object} sprite Example: {spriteName: [offset, duration, loop]} + * @param {Integer} offset Where to begin playback in milliseconds + * @param {Integer} duration How long to play in milliseconds + * @param {Boolean} loop (optional) Set true to loop this sprite + * @return {Howl} Returns current sprite sheet or self. + */ + sprite: function(sprite) { + var self = this; + + if (typeof sprite === 'object') { + self._sprite = sprite; + + return self; + } else { + return self._sprite; + } + }, + + /** + * Get/set the position of playback. + * @param {Float} pos The position to move current playback to. + * @param {String} id (optional) The play instance ID. + * @return {Howl/Float} Returns self or current playback position. + */ + pos: function(pos, id) { + var self = this; + + // if the sound hasn't been loaded, add it to the event queue + if (!self._loaded) { + self.on('load', function() { + self.pos(pos); + }); + + return typeof pos === 'number' ? self : self._pos || 0; + } + + // make sure we are dealing with a number for pos + pos = parseFloat(pos); + + var activeNode = (id) ? self._nodeById(id) : self._activeNode(); + if (activeNode) { + if (pos >= 0) { + self.pause(id); + activeNode._pos = pos; + self.play(activeNode._sprite, id); + + return self; + } else { + return self._webAudio ? activeNode._pos + (ctx.currentTime - self._playStart) : activeNode.currentTime; + } + } else if (pos >= 0) { + return self; + } else { + // find the first inactive node to return the pos for + for (var i=0; i= 0 || x < 0) { + if (self._webAudio) { + var activeNode = (id) ? self._nodeById(id) : self._activeNode(); + if (activeNode) { + self._pos3d = [x, y, z]; + activeNode.panner.setPosition(x, y, z); + activeNode.panner.panningModel = self._model || 'HRTF'; + } + } + } else { + return self._pos3d; + } + + return self; + }, + + /** + * Fade a currently playing sound between two volumes. + * @param {Number} from The volume to fade from (0.0 to 1.0). + * @param {Number} to The volume to fade to (0.0 to 1.0). + * @param {Number} len Time in milliseconds to fade. + * @param {Function} callback (optional) Fired when the fade is complete. + * @param {String} id (optional) The play instance ID. + * @return {Howl} + */ + fade: function(from, to, len, callback, id) { + var self = this, + diff = Math.abs(from - to), + dir = from > to ? 'down' : 'up', + steps = diff / 0.01, + stepTime = len / steps; + + // if the sound hasn't been loaded, add it to the event queue + if (!self._loaded) { + self.on('load', function() { + self.fade(from, to, len, callback, id); + }); + + return self; + } + + // set the volume to the start position + self.volume(from, id); + + for (var i=1; i<=steps; i++) { + (function() { + var change = self._volume + (dir === 'up' ? 0.01 : -0.01) * i, + vol = Math.round(1000 * change) / 1000, + toVol = to; + + setTimeout(function() { + self.volume(vol, id); + + if (vol === toVol) { + if (callback) callback(); + } + }, stepTime * i); + })(); + } + }, + + /** + * [DEPRECATED] Fade in the current sound. + * @param {Float} to Volume to fade to (0.0 to 1.0). + * @param {Number} len Time in milliseconds to fade. + * @param {Function} callback + * @return {Howl} + */ + fadeIn: function(to, len, callback) { + return this.volume(0).play().fade(0, to, len, callback); + }, + + /** + * [DEPRECATED] Fade out the current sound and pause when finished. + * @param {Float} to Volume to fade to (0.0 to 1.0). + * @param {Number} len Time in milliseconds to fade. + * @param {Function} callback + * @param {String} id (optional) The play instance ID. + * @return {Howl} + */ + fadeOut: function(to, len, callback, id) { + var self = this; + + return self.fade(self._volume, to, len, function() { + if (callback) callback(); + self.pause(id); + + // fire ended event + self.on('end'); + }, id); + }, + + /** + * Get an audio node by ID. + * @return {Howl} Audio node. + */ + _nodeById: function(id) { + var self = this, + node = self._audioNode[0]; + + // find the node with this ID + for (var i=0; i=0; i--) { + if (inactive <= 5) { + break; + } + + if (self._audioNode[i].paused) { + // disconnect the audio source if using Web Audio + if (self._webAudio) { + self._audioNode[i].disconnect(0); + } + + inactive--; + self._audioNode.splice(i, 1); + } + } + }, + + /** + * Clear 'onend' timeout before it ends. + * @param {String} soundId The play instance ID. + */ + _clearEndTimer: function(soundId) { + var self = this, + index = 0; + + // loop through the timers to find the one associated with this sound + for (var i=0; i= 0) { + Howler._howls.splice(index, 1); + } + + // delete this sound from the cache + delete cache[self._src]; + self = null; + } + + }; + + // only define these functions when using WebAudio + if (usingWebAudio) { + + /** + * Buffer a sound from URL (or from cache) and decode to audio source (Web Audio API). + * @param {Object} obj The Howl object for the sound to load. + * @param {String} url The path to the sound file. + */ + var loadBuffer = function(obj, url) { + // check if the buffer has already been cached + if (url in cache) { + // set the duration from the cache + obj._duration = cache[url].duration; + + // load the sound into this object + loadSound(obj); + return; + } + + if (/^data:[^;]+;base64,/.test(url)) { + // Decode base64 data-URIs because some browsers cannot load data-URIs with XMLHttpRequest. + var data = atob(url.split(',')[1]); + var dataView = new Uint8Array(data.length); + for (var i=0; i= 26) || + (window.navigator.userAgent.match('Firefox') && parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10) >= 33)); +var AudioContext = window.AudioContext || window.webkitAudioContext; +var videoEl = document.createElement('video'); +var supportVp8 = videoEl && videoEl.canPlayType && videoEl.canPlayType('video/webm; codecs="vp8", vorbis') === "probably"; +var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.msGetUserMedia || navigator.mozGetUserMedia; + +// export support flags and constructors.prototype && PC +module.exports = { + prefix: prefix, + support: !!PC && supportVp8 && !!getUserMedia, + // new support style + supportRTCPeerConnection: !!PC, + supportVp8: supportVp8, + supportGetUserMedia: !!getUserMedia, + supportDataChannel: !!(PC && PC.prototype && PC.prototype.createDataChannel), + supportWebAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), + supportMediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), + supportScreenSharing: !!screenSharing, + // old deprecated style. Dont use this anymore + dataChannel: !!(PC && PC.prototype && PC.prototype.createDataChannel), + webAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), + mediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), + screenSharing: !!screenSharing, + // constructors + AudioContext: AudioContext, + PeerConnection: PC, + SessionDescription: SessionDescription, + IceCandidate: IceCandidate, + MediaStream: MediaStream, + getUserMedia: getUserMedia +}; + +},{}],6:[function(require,module,exports){ +module.exports = function (stream, el, options) { + var URL = window.URL; + var opts = { + autoplay: true, + mirror: false, + muted: false + }; + var element = el || document.createElement('video'); + var item; + + if (options) { + for (item in options) { + opts[item] = options[item]; + } + } + + if (opts.autoplay) element.autoplay = 'autoplay'; + if (opts.muted) element.muted = true; + if (opts.mirror) { + ['', 'moz', 'webkit', 'o', 'ms'].forEach(function (prefix) { + var styleName = prefix ? prefix + 'Transform' : 'transform'; + element.style[styleName] = 'scaleX(-1)'; + }); + } + + // this first one should work most everywhere now + // but we have a few fallbacks just in case. + if (URL && URL.createObjectURL) { + element.src = URL.createObjectURL(stream); + } else if (element.srcObject) { + element.srcObject = stream; + } else if (element.mozSrcObject) { + element.mozSrcObject = stream; + } else { + return false; + } + + return element; +}; + +},{}],7:[function(require,module,exports){ +var methods = "assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","); +var l = methods.length; +var fn = function () {}; +var mockconsole = {}; + +while (l--) { + mockconsole[methods[l]] = fn; +} + +module.exports = mockconsole; + +},{}],2:[function(require,module,exports){ +var io = require('socket.io-client'); + +function SocketIoConnection(config) { + this.connection = io.connect(config.url, config.socketio); +} + +SocketIoConnection.prototype.on = function (ev, fn) { + this.connection.on(ev, fn); +}; + +SocketIoConnection.prototype.emit = function () { + this.connection.emit.apply(this.connection, arguments); +}; + +SocketIoConnection.prototype.getSessionid = function () { + return this.connection.socket.sessionid; +}; + +SocketIoConnection.prototype.disconnect = function () { + return this.connection.disconnect(); +}; + +module.exports = SocketIoConnection; + +},{"socket.io-client":8}],8:[function(require,module,exports){ +/*! Socket.IO.js build:0.9.16, development. Copyright(c) 2011 LearnBoost MIT Licensed */ + +var io = ('undefined' === typeof module ? {} : module.exports); +(function() { + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, global) { + + /** + * IO namespace. + * + * @namespace + */ + + var io = exports; + + /** + * Socket.IO version + * + * @api public + */ + + io.version = '0.9.16'; + + /** + * Protocol implemented. + * + * @api public + */ + + io.protocol = 1; + + /** + * Available transports, these will be populated with the available transports + * + * @api public + */ + + io.transports = []; + + /** + * Keep track of jsonp callbacks. + * + * @api private + */ + + io.j = []; + + /** + * Keep track of our io.Sockets + * + * @api private + */ + io.sockets = {}; + + + /** + * Manages connections to hosts. + * + * @param {String} uri + * @Param {Boolean} force creation of new socket (defaults to false) + * @api public + */ + + io.connect = function (host, details) { + var uri = io.util.parseUri(host) + , uuri + , socket; + + if (global && global.location) { + uri.protocol = uri.protocol || global.location.protocol.slice(0, -1); + uri.host = uri.host || (global.document + ? global.document.domain : global.location.hostname); + uri.port = uri.port || global.location.port; + } + + uuri = io.util.uniqueUri(uri); + + var options = { + host: uri.host + , secure: 'https' == uri.protocol + , port: uri.port || ('https' == uri.protocol ? 443 : 80) + , query: uri.query || '' + }; + + io.util.merge(options, details); + + if (options['force new connection'] || !io.sockets[uuri]) { + socket = new io.Socket(options); + } + + if (!options['force new connection'] && socket) { + io.sockets[uuri] = socket; + } + + socket = socket || io.sockets[uuri]; + + // if path is different from '' or / + return socket.of(uri.path.length > 1 ? uri.path : ''); + }; + +})('object' === typeof module ? module.exports : (this.io = {}), this); +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, global) { + + /** + * Utilities namespace. + * + * @namespace + */ + + var util = exports.util = {}; + + /** + * Parses an URI + * + * @author Steven Levithan (MIT license) + * @api public + */ + + var re = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/; + + var parts = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', + 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', + 'anchor']; + + util.parseUri = function (str) { + var m = re.exec(str || '') + , uri = {} + , i = 14; + + while (i--) { + uri[parts[i]] = m[i] || ''; + } + + return uri; + }; + + /** + * Produces a unique url that identifies a Socket.IO connection. + * + * @param {Object} uri + * @api public + */ + + util.uniqueUri = function (uri) { + var protocol = uri.protocol + , host = uri.host + , port = uri.port; + + if ('document' in global) { + host = host || document.domain; + port = port || (protocol == 'https' + && document.location.protocol !== 'https:' ? 443 : document.location.port); + } else { + host = host || 'localhost'; + + if (!port && protocol == 'https') { + port = 443; + } + } + + return (protocol || 'http') + '://' + host + ':' + (port || 80); + }; + + /** + * Mergest 2 query strings in to once unique query string + * + * @param {String} base + * @param {String} addition + * @api public + */ + + util.query = function (base, addition) { + var query = util.chunkQuery(base || '') + , components = []; + + util.merge(query, util.chunkQuery(addition || '')); + for (var part in query) { + if (query.hasOwnProperty(part)) { + components.push(part + '=' + query[part]); + } + } + + return components.length ? '?' + components.join('&') : ''; + }; + + /** + * Transforms a querystring in to an object + * + * @param {String} qs + * @api public + */ + + util.chunkQuery = function (qs) { + var query = {} + , params = qs.split('&') + , i = 0 + , l = params.length + , kv; + + for (; i < l; ++i) { + kv = params[i].split('='); + if (kv[0]) { + query[kv[0]] = kv[1]; + } + } + + return query; + }; + + /** + * Executes the given function when the page is loaded. + * + * io.util.load(function () { console.log('page loaded'); }); + * + * @param {Function} fn + * @api public + */ + + var pageLoaded = false; + + util.load = function (fn) { + if ('document' in global && document.readyState === 'complete' || pageLoaded) { + return fn(); + } + + util.on(global, 'load', fn, false); + }; + + /** + * Adds an event. + * + * @api private + */ + + util.on = function (element, event, fn, capture) { + if (element.attachEvent) { + element.attachEvent('on' + event, fn); + } else if (element.addEventListener) { + element.addEventListener(event, fn, capture); + } + }; + + /** + * Generates the correct `XMLHttpRequest` for regular and cross domain requests. + * + * @param {Boolean} [xdomain] Create a request that can be used cross domain. + * @returns {XMLHttpRequest|false} If we can create a XMLHttpRequest. + * @api private + */ + + util.request = function (xdomain) { + + if (xdomain && 'undefined' != typeof XDomainRequest && !util.ua.hasCORS) { + return new XDomainRequest(); + } + + if ('undefined' != typeof XMLHttpRequest && (!xdomain || util.ua.hasCORS)) { + return new XMLHttpRequest(); + } + + if (!xdomain) { + try { + return new window[(['Active'].concat('Object').join('X'))]('Microsoft.XMLHTTP'); + } catch(e) { } + } + + return null; + }; + + /** + * XHR based transport constructor. + * + * @constructor + * @api public + */ + + /** + * Change the internal pageLoaded value. + */ + + if ('undefined' != typeof window) { + util.load(function () { + pageLoaded = true; + }); + } + + /** + * Defers a function to ensure a spinner is not displayed by the browser + * + * @param {Function} fn + * @api public + */ + + util.defer = function (fn) { + if (!util.ua.webkit || 'undefined' != typeof importScripts) { + return fn(); + } + + util.load(function () { + setTimeout(fn, 100); + }); + }; + + /** + * Merges two objects. + * + * @api public + */ + + util.merge = function merge (target, additional, deep, lastseen) { + var seen = lastseen || [] + , depth = typeof deep == 'undefined' ? 2 : deep + , prop; + + for (prop in additional) { + if (additional.hasOwnProperty(prop) && util.indexOf(seen, prop) < 0) { + if (typeof target[prop] !== 'object' || !depth) { + target[prop] = additional[prop]; + seen.push(additional[prop]); + } else { + util.merge(target[prop], additional[prop], depth - 1, seen); + } + } + } + + return target; + }; + + /** + * Merges prototypes from objects + * + * @api public + */ + + util.mixin = function (ctor, ctor2) { + util.merge(ctor.prototype, ctor2.prototype); + }; + + /** + * Shortcut for prototypical and static inheritance. + * + * @api private + */ + + util.inherit = function (ctor, ctor2) { + function f() {}; + f.prototype = ctor2.prototype; + ctor.prototype = new f; + }; + + /** + * Checks if the given object is an Array. + * + * io.util.isArray([]); // true + * io.util.isArray({}); // false + * + * @param Object obj + * @api public + */ + + util.isArray = Array.isArray || function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; + + /** + * Intersects values of two arrays into a third + * + * @api public + */ + + util.intersect = function (arr, arr2) { + var ret = [] + , longest = arr.length > arr2.length ? arr : arr2 + , shortest = arr.length > arr2.length ? arr2 : arr; + + for (var i = 0, l = shortest.length; i < l; i++) { + if (~util.indexOf(longest, shortest[i])) + ret.push(shortest[i]); + } + + return ret; + }; + + /** + * Array indexOf compatibility. + * + * @see bit.ly/a5Dxa2 + * @api public + */ + + util.indexOf = function (arr, o, i) { + + for (var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0; + i < j && arr[i] !== o; i++) {} + + return j <= i ? -1 : i; + }; + + /** + * Converts enumerables to array. + * + * @api public + */ + + util.toArray = function (enu) { + var arr = []; + + for (var i = 0, l = enu.length; i < l; i++) + arr.push(enu[i]); + + return arr; + }; + + /** + * UA / engines detection namespace. + * + * @namespace + */ + + util.ua = {}; + + /** + * Whether the UA supports CORS for XHR. + * + * @api public + */ + + util.ua.hasCORS = 'undefined' != typeof XMLHttpRequest && (function () { + try { + var a = new XMLHttpRequest(); + } catch (e) { + return false; + } + + return a.withCredentials != undefined; + })(); + + /** + * Detect webkit. + * + * @api public + */ + + util.ua.webkit = 'undefined' != typeof navigator + && /webkit/i.test(navigator.userAgent); + + /** + * Detect iPad/iPhone/iPod. + * + * @api public + */ + + util.ua.iDevice = 'undefined' != typeof navigator + && /iPad|iPhone|iPod/i.test(navigator.userAgent); + +})('undefined' != typeof io ? io : module.exports, this); +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.EventEmitter = EventEmitter; + + /** + * Event emitter constructor. + * + * @api public. + */ + + function EventEmitter () {}; + + /** + * Adds a listener + * + * @api public + */ + + EventEmitter.prototype.on = function (name, fn) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = fn; + } else if (io.util.isArray(this.$events[name])) { + this.$events[name].push(fn); + } else { + this.$events[name] = [this.$events[name], fn]; + } + + return this; + }; + + EventEmitter.prototype.addListener = EventEmitter.prototype.on; + + /** + * Adds a volatile listener. + * + * @api public + */ + + EventEmitter.prototype.once = function (name, fn) { + var self = this; + + function on () { + self.removeListener(name, on); + fn.apply(this, arguments); + }; + + on.listener = fn; + this.on(name, on); + + return this; + }; + + /** + * Removes a listener. + * + * @api public + */ + + EventEmitter.prototype.removeListener = function (name, fn) { + if (this.$events && this.$events[name]) { + var list = this.$events[name]; + + if (io.util.isArray(list)) { + var pos = -1; + + for (var i = 0, l = list.length; i < l; i++) { + if (list[i] === fn || (list[i].listener && list[i].listener === fn)) { + pos = i; + break; + } + } + + if (pos < 0) { + return this; + } + + list.splice(pos, 1); + + if (!list.length) { + delete this.$events[name]; + } + } else if (list === fn || (list.listener && list.listener === fn)) { + delete this.$events[name]; + } + } + + return this; + }; + + /** + * Removes all listeners for an event. + * + * @api public + */ + + EventEmitter.prototype.removeAllListeners = function (name) { + if (name === undefined) { + this.$events = {}; + return this; + } + + if (this.$events && this.$events[name]) { + this.$events[name] = null; + } + + return this; + }; + + /** + * Gets all listeners for a certain event. + * + * @api publci + */ + + EventEmitter.prototype.listeners = function (name) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = []; + } + + if (!io.util.isArray(this.$events[name])) { + this.$events[name] = [this.$events[name]]; + } + + return this.$events[name]; + }; + + /** + * Emits an event. + * + * @api public + */ + + EventEmitter.prototype.emit = function (name) { + if (!this.$events) { + return false; + } + + var handler = this.$events[name]; + + if (!handler) { + return false; + } + + var args = Array.prototype.slice.call(arguments, 1); + + if ('function' == typeof handler) { + handler.apply(this, args); + } else if (io.util.isArray(handler)) { + var listeners = handler.slice(); + + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + } else { + return false; + } + + return true; + }; + +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Based on JSON2 (http://www.JSON.org/js.html). + */ + +(function (exports, nativeJSON) { + "use strict"; + + // use native JSON if it's available + if (nativeJSON && nativeJSON.parse){ + return exports.JSON = { + parse: nativeJSON.parse + , stringify: nativeJSON.stringify + }; + } + + var JSON = exports.JSON = {}; + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + function date(d, key) { + return isFinite(d.valueOf()) ? + d.getUTCFullYear() + '-' + + f(d.getUTCMonth() + 1) + '-' + + f(d.getUTCDate()) + 'T' + + f(d.getUTCHours()) + ':' + + f(d.getUTCMinutes()) + ':' + + f(d.getUTCSeconds()) + 'Z' : null; + }; + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value instanceof Date) { + value = date(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : gap ? + '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : gap ? + '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : + '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + +// If the JSON object does not yet have a parse method, give it one. + + JSON.parse = function (text, reviver) { + // The parse method takes a text and an optional reviver function, and returns + // a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + + // The walk method is used to recursively walk the resulting structure so + // that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + + // Parsing happens in four stages. In the first stage, we replace certain + // Unicode characters with escape sequences. JavaScript handles many characters + // incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + + // In the second stage, we run the text against regular expressions that look + // for non-JSON patterns. We are especially concerned with '()' and 'new' + // because they can cause invocation, and '=' because it can cause mutation. + // But just to be safe, we want to reject all unexpected forms. + + // We split the second stage into 4 regexp operations in order to work around + // crippling inefficiencies in IE's and Safari's regexp engines. First we + // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we + // replace all simple value tokens with ']' characters. Third, we delete all + // open brackets that follow a colon or comma or that begin the text. Finally, + // we look to see that the remaining characters are only whitespace or ']' or + // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ + .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + + // In the third stage we use the eval function to compile the text into a + // JavaScript structure. The '{' operator is subject to a syntactic ambiguity + // in JavaScript: it can begin a block or an object literal. We wrap the text + // in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + + // In the optional fourth stage, we recursively walk the new structure, passing + // each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + + // If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + +})( + 'undefined' != typeof io ? io : module.exports + , typeof JSON !== 'undefined' ? JSON : undefined +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Parser namespace. + * + * @namespace + */ + + var parser = exports.parser = {}; + + /** + * Packet types. + */ + + var packets = parser.packets = [ + 'disconnect' + , 'connect' + , 'heartbeat' + , 'message' + , 'json' + , 'event' + , 'ack' + , 'error' + , 'noop' + ]; + + /** + * Errors reasons. + */ + + var reasons = parser.reasons = [ + 'transport not supported' + , 'client not handshaken' + , 'unauthorized' + ]; + + /** + * Errors advice. + */ + + var advice = parser.advice = [ + 'reconnect' + ]; + + /** + * Shortcuts. + */ + + var JSON = io.JSON + , indexOf = io.util.indexOf; + + /** + * Encodes a packet. + * + * @api private + */ + + parser.encodePacket = function (packet) { + var type = indexOf(packets, packet.type) + , id = packet.id || '' + , endpoint = packet.endpoint || '' + , ack = packet.ack + , data = null; + + switch (packet.type) { + case 'error': + var reason = packet.reason ? indexOf(reasons, packet.reason) : '' + , adv = packet.advice ? indexOf(advice, packet.advice) : ''; + + if (reason !== '' || adv !== '') + data = reason + (adv !== '' ? ('+' + adv) : ''); + + break; + + case 'message': + if (packet.data !== '') + data = packet.data; + break; + + case 'event': + var ev = { name: packet.name }; + + if (packet.args && packet.args.length) { + ev.args = packet.args; + } + + data = JSON.stringify(ev); + break; + + case 'json': + data = JSON.stringify(packet.data); + break; + + case 'connect': + if (packet.qs) + data = packet.qs; + break; + + case 'ack': + data = packet.ackId + + (packet.args && packet.args.length + ? '+' + JSON.stringify(packet.args) : ''); + break; + } + + // construct packet with required fragments + var encoded = [ + type + , id + (ack == 'data' ? '+' : '') + , endpoint + ]; + + // data fragment is optional + if (data !== null && data !== undefined) + encoded.push(data); + + return encoded.join(':'); + }; + + /** + * Encodes multiple messages (payload). + * + * @param {Array} messages + * @api private + */ + + parser.encodePayload = function (packets) { + var decoded = ''; + + if (packets.length == 1) + return packets[0]; + + for (var i = 0, l = packets.length; i < l; i++) { + var packet = packets[i]; + decoded += '\ufffd' + packet.length + '\ufffd' + packets[i]; + } + + return decoded; + }; + + /** + * Decodes a packet + * + * @api private + */ + + var regexp = /([^:]+):([0-9]+)?(\+)?:([^:]+)?:?([\s\S]*)?/; + + parser.decodePacket = function (data) { + var pieces = data.match(regexp); + + if (!pieces) return {}; + + var id = pieces[2] || '' + , data = pieces[5] || '' + , packet = { + type: packets[pieces[1]] + , endpoint: pieces[4] || '' + }; + + // whether we need to acknowledge the packet + if (id) { + packet.id = id; + if (pieces[3]) + packet.ack = 'data'; + else + packet.ack = true; + } + + // handle different packet types + switch (packet.type) { + case 'error': + var pieces = data.split('+'); + packet.reason = reasons[pieces[0]] || ''; + packet.advice = advice[pieces[1]] || ''; + break; + + case 'message': + packet.data = data || ''; + break; + + case 'event': + try { + var opts = JSON.parse(data); + packet.name = opts.name; + packet.args = opts.args; + } catch (e) { } + + packet.args = packet.args || []; + break; + + case 'json': + try { + packet.data = JSON.parse(data); + } catch (e) { } + break; + + case 'connect': + packet.qs = data || ''; + break; + + case 'ack': + var pieces = data.match(/^([0-9]+)(\+)?(.*)/); + if (pieces) { + packet.ackId = pieces[1]; + packet.args = []; + + if (pieces[3]) { + try { + packet.args = pieces[3] ? JSON.parse(pieces[3]) : []; + } catch (e) { } + } + } + break; + + case 'disconnect': + case 'heartbeat': + break; + }; + + return packet; + }; + + /** + * Decodes data payload. Detects multiple messages + * + * @return {Array} messages + * @api public + */ + + parser.decodePayload = function (data) { + // IE doesn't like data[i] for unicode chars, charAt works fine + if (data.charAt(0) == '\ufffd') { + var ret = []; + + for (var i = 1, length = ''; i < data.length; i++) { + if (data.charAt(i) == '\ufffd') { + ret.push(parser.decodePacket(data.substr(i + 1).substr(0, length))); + i += Number(length) + 1; + length = ''; + } else { + length += data.charAt(i); + } + } + + return ret; + } else { + return [parser.decodePacket(data)]; + } + }; + +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.Transport = Transport; + + /** + * This is the transport template for all supported transport methods. + * + * @constructor + * @api public + */ + + function Transport (socket, sessid) { + this.socket = socket; + this.sessid = sessid; + }; + + /** + * Apply EventEmitter mixin. + */ + + io.util.mixin(Transport, io.EventEmitter); + + + /** + * Indicates whether heartbeats is enabled for this transport + * + * @api private + */ + + Transport.prototype.heartbeats = function () { + return true; + }; + + /** + * Handles the response from the server. When a new response is received + * it will automatically update the timeout, decode the message and + * forwards the response to the onMessage function for further processing. + * + * @param {String} data Response from the server. + * @api private + */ + + Transport.prototype.onData = function (data) { + this.clearCloseTimeout(); + + // If the connection in currently open (or in a reopening state) reset the close + // timeout since we have just received data. This check is necessary so + // that we don't reset the timeout on an explicitly disconnected connection. + if (this.socket.connected || this.socket.connecting || this.socket.reconnecting) { + this.setCloseTimeout(); + } + + if (data !== '') { + // todo: we should only do decodePayload for xhr transports + var msgs = io.parser.decodePayload(data); + + if (msgs && msgs.length) { + for (var i = 0, l = msgs.length; i < l; i++) { + this.onPacket(msgs[i]); + } + } + } + + return this; + }; + + /** + * Handles packets. + * + * @api private + */ + + Transport.prototype.onPacket = function (packet) { + this.socket.setHeartbeatTimeout(); + + if (packet.type == 'heartbeat') { + return this.onHeartbeat(); + } + + if (packet.type == 'connect' && packet.endpoint == '') { + this.onConnect(); + } + + if (packet.type == 'error' && packet.advice == 'reconnect') { + this.isOpen = false; + } + + this.socket.onPacket(packet); + + return this; + }; + + /** + * Sets close timeout + * + * @api private + */ + + Transport.prototype.setCloseTimeout = function () { + if (!this.closeTimeout) { + var self = this; + + this.closeTimeout = setTimeout(function () { + self.onDisconnect(); + }, this.socket.closeTimeout); + } + }; + + /** + * Called when transport disconnects. + * + * @api private + */ + + Transport.prototype.onDisconnect = function () { + if (this.isOpen) this.close(); + this.clearTimeouts(); + this.socket.onDisconnect(); + return this; + }; + + /** + * Called when transport connects + * + * @api private + */ + + Transport.prototype.onConnect = function () { + this.socket.onConnect(); + return this; + }; + + /** + * Clears close timeout + * + * @api private + */ + + Transport.prototype.clearCloseTimeout = function () { + if (this.closeTimeout) { + clearTimeout(this.closeTimeout); + this.closeTimeout = null; + } + }; + + /** + * Clear timeouts + * + * @api private + */ + + Transport.prototype.clearTimeouts = function () { + this.clearCloseTimeout(); + + if (this.reopenTimeout) { + clearTimeout(this.reopenTimeout); + } + }; + + /** + * Sends a packet + * + * @param {Object} packet object. + * @api private + */ + + Transport.prototype.packet = function (packet) { + this.send(io.parser.encodePacket(packet)); + }; + + /** + * Send the received heartbeat message back to server. So the server + * knows we are still connected. + * + * @param {String} heartbeat Heartbeat response from the server. + * @api private + */ + + Transport.prototype.onHeartbeat = function (heartbeat) { + this.packet({ type: 'heartbeat' }); + }; + + /** + * Called when the transport opens. + * + * @api private + */ + + Transport.prototype.onOpen = function () { + this.isOpen = true; + this.clearCloseTimeout(); + this.socket.onOpen(); + }; + + /** + * Notifies the base when the connection with the Socket.IO server + * has been disconnected. + * + * @api private + */ + + Transport.prototype.onClose = function () { + var self = this; + + /* FIXME: reopen delay causing a infinit loop + this.reopenTimeout = setTimeout(function () { + self.open(); + }, this.socket.options['reopen delay']);*/ + + this.isOpen = false; + this.socket.onClose(); + this.onDisconnect(); + }; + + /** + * Generates a connection url based on the Socket.IO URL Protocol. + * See for more details. + * + * @returns {String} Connection url + * @api private + */ + + Transport.prototype.prepareUrl = function () { + var options = this.socket.options; + + return this.scheme() + '://' + + options.host + ':' + options.port + '/' + + options.resource + '/' + io.protocol + + '/' + this.name + '/' + this.sessid; + }; + + /** + * Checks if the transport is ready to start a connection. + * + * @param {Socket} socket The socket instance that needs a transport + * @param {Function} fn The callback + * @api private + */ + + Transport.prototype.ready = function (socket, fn) { + fn.call(this); + }; +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io, global) { + + /** + * Expose constructor. + */ + + exports.Socket = Socket; + + /** + * Create a new `Socket.IO client` which can establish a persistent + * connection with a Socket.IO enabled server. + * + * @api public + */ + + function Socket (options) { + this.options = { + port: 80 + , secure: false + , document: 'document' in global ? document : false + , resource: 'socket.io' + , transports: io.transports + , 'connect timeout': 10000 + , 'try multiple transports': true + , 'reconnect': true + , 'reconnection delay': 500 + , 'reconnection limit': Infinity + , 'reopen delay': 3000 + , 'max reconnection attempts': 10 + , 'sync disconnect on unload': false + , 'auto connect': true + , 'flash policy port': 10843 + , 'manualFlush': false + }; + + io.util.merge(this.options, options); + + this.connected = false; + this.open = false; + this.connecting = false; + this.reconnecting = false; + this.namespaces = {}; + this.buffer = []; + this.doBuffer = false; + + if (this.options['sync disconnect on unload'] && + (!this.isXDomain() || io.util.ua.hasCORS)) { + var self = this; + io.util.on(global, 'beforeunload', function () { + self.disconnectSync(); + }, false); + } + + if (this.options['auto connect']) { + this.connect(); + } +}; + + /** + * Apply EventEmitter mixin. + */ + + io.util.mixin(Socket, io.EventEmitter); + + /** + * Returns a namespace listener/emitter for this socket + * + * @api public + */ + + Socket.prototype.of = function (name) { + if (!this.namespaces[name]) { + this.namespaces[name] = new io.SocketNamespace(this, name); + + if (name !== '') { + this.namespaces[name].packet({ type: 'connect' }); + } + } + + return this.namespaces[name]; + }; + + /** + * Emits the given event to the Socket and all namespaces + * + * @api private + */ + + Socket.prototype.publish = function () { + this.emit.apply(this, arguments); + + var nsp; + + for (var i in this.namespaces) { + if (this.namespaces.hasOwnProperty(i)) { + nsp = this.of(i); + nsp.$emit.apply(nsp, arguments); + } + } + }; + + /** + * Performs the handshake + * + * @api private + */ + + function empty () { }; + + Socket.prototype.handshake = function (fn) { + var self = this + , options = this.options; + + function complete (data) { + if (data instanceof Error) { + self.connecting = false; + self.onError(data.message); + } else { + fn.apply(null, data.split(':')); + } + }; + + var url = [ + 'http' + (options.secure ? 's' : '') + ':/' + , options.host + ':' + options.port + , options.resource + , io.protocol + , io.util.query(this.options.query, 't=' + +new Date) + ].join('/'); + + if (this.isXDomain() && !io.util.ua.hasCORS) { + var insertAt = document.getElementsByTagName('script')[0] + , script = document.createElement('script'); + + script.src = url + '&jsonp=' + io.j.length; + insertAt.parentNode.insertBefore(script, insertAt); + + io.j.push(function (data) { + complete(data); + script.parentNode.removeChild(script); + }); + } else { + var xhr = io.util.request(); + + xhr.open('GET', url, true); + if (this.isXDomain()) { + xhr.withCredentials = true; + } + xhr.onreadystatechange = function () { + if (xhr.readyState == 4) { + xhr.onreadystatechange = empty; + + if (xhr.status == 200) { + complete(xhr.responseText); + } else if (xhr.status == 403) { + self.onError(xhr.responseText); + } else { + self.connecting = false; + !self.reconnecting && self.onError(xhr.responseText); + } + } + }; + xhr.send(null); + } + }; + + /** + * Find an available transport based on the options supplied in the constructor. + * + * @api private + */ + + Socket.prototype.getTransport = function (override) { + var transports = override || this.transports, match; + + for (var i = 0, transport; transport = transports[i]; i++) { + if (io.Transport[transport] + && io.Transport[transport].check(this) + && (!this.isXDomain() || io.Transport[transport].xdomainCheck(this))) { + return new io.Transport[transport](this, this.sessionid); + } + } + + return null; + }; + + /** + * Connects to the server. + * + * @param {Function} [fn] Callback. + * @returns {io.Socket} + * @api public + */ + + Socket.prototype.connect = function (fn) { + if (this.connecting) { + return this; + } + + var self = this; + self.connecting = true; + + this.handshake(function (sid, heartbeat, close, transports) { + self.sessionid = sid; + self.closeTimeout = close * 1000; + self.heartbeatTimeout = heartbeat * 1000; + if(!self.transports) + self.transports = self.origTransports = (transports ? io.util.intersect( + transports.split(',') + , self.options.transports + ) : self.options.transports); + + self.setHeartbeatTimeout(); + + function connect (transports){ + if (self.transport) self.transport.clearTimeouts(); + + self.transport = self.getTransport(transports); + if (!self.transport) return self.publish('connect_failed'); + + // once the transport is ready + self.transport.ready(self, function () { + self.connecting = true; + self.publish('connecting', self.transport.name); + self.transport.open(); + + if (self.options['connect timeout']) { + self.connectTimeoutTimer = setTimeout(function () { + if (!self.connected) { + self.connecting = false; + + if (self.options['try multiple transports']) { + var remaining = self.transports; + + while (remaining.length > 0 && remaining.splice(0,1)[0] != + self.transport.name) {} + + if (remaining.length){ + connect(remaining); + } else { + self.publish('connect_failed'); + } + } + } + }, self.options['connect timeout']); + } + }); + } + + connect(self.transports); + + self.once('connect', function (){ + clearTimeout(self.connectTimeoutTimer); + + fn && typeof fn == 'function' && fn(); + }); + }); + + return this; + }; + + /** + * Clears and sets a new heartbeat timeout using the value given by the + * server during the handshake. + * + * @api private + */ + + Socket.prototype.setHeartbeatTimeout = function () { + clearTimeout(this.heartbeatTimeoutTimer); + if(this.transport && !this.transport.heartbeats()) return; + + var self = this; + this.heartbeatTimeoutTimer = setTimeout(function () { + self.transport.onClose(); + }, this.heartbeatTimeout); + }; + + /** + * Sends a message. + * + * @param {Object} data packet. + * @returns {io.Socket} + * @api public + */ + + Socket.prototype.packet = function (data) { + if (this.connected && !this.doBuffer) { + this.transport.packet(data); + } else { + this.buffer.push(data); + } + + return this; + }; + + /** + * Sets buffer state + * + * @api private + */ + + Socket.prototype.setBuffer = function (v) { + this.doBuffer = v; + + if (!v && this.connected && this.buffer.length) { + if (!this.options['manualFlush']) { + this.flushBuffer(); + } + } + }; + + /** + * Flushes the buffer data over the wire. + * To be invoked manually when 'manualFlush' is set to true. + * + * @api public + */ + + Socket.prototype.flushBuffer = function() { + this.transport.payload(this.buffer); + this.buffer = []; + }; + + + /** + * Disconnect the established connect. + * + * @returns {io.Socket} + * @api public + */ + + Socket.prototype.disconnect = function () { + if (this.connected || this.connecting) { + if (this.open) { + this.of('').packet({ type: 'disconnect' }); + } + + // handle disconnection immediately + this.onDisconnect('booted'); + } + + return this; + }; + + /** + * Disconnects the socket with a sync XHR. + * + * @api private + */ + + Socket.prototype.disconnectSync = function () { + // ensure disconnection + var xhr = io.util.request(); + var uri = [ + 'http' + (this.options.secure ? 's' : '') + ':/' + , this.options.host + ':' + this.options.port + , this.options.resource + , io.protocol + , '' + , this.sessionid + ].join('/') + '/?disconnect=1'; + + xhr.open('GET', uri, false); + xhr.send(null); + + // handle disconnection immediately + this.onDisconnect('booted'); + }; + + /** + * Check if we need to use cross domain enabled transports. Cross domain would + * be a different port or different domain name. + * + * @returns {Boolean} + * @api private + */ + + Socket.prototype.isXDomain = function () { + + var port = global.location.port || + ('https:' == global.location.protocol ? 443 : 80); + + return this.options.host !== global.location.hostname + || this.options.port != port; + }; + + /** + * Called upon handshake. + * + * @api private + */ + + Socket.prototype.onConnect = function () { + if (!this.connected) { + this.connected = true; + this.connecting = false; + if (!this.doBuffer) { + // make sure to flush the buffer + this.setBuffer(false); + } + this.emit('connect'); + } + }; + + /** + * Called when the transport opens + * + * @api private + */ + + Socket.prototype.onOpen = function () { + this.open = true; + }; + + /** + * Called when the transport closes. + * + * @api private + */ + + Socket.prototype.onClose = function () { + this.open = false; + clearTimeout(this.heartbeatTimeoutTimer); + }; + + /** + * Called when the transport first opens a connection + * + * @param text + */ + + Socket.prototype.onPacket = function (packet) { + this.of(packet.endpoint).onPacket(packet); + }; + + /** + * Handles an error. + * + * @api private + */ + + Socket.prototype.onError = function (err) { + if (err && err.advice) { + if (err.advice === 'reconnect' && (this.connected || this.connecting)) { + this.disconnect(); + if (this.options.reconnect) { + this.reconnect(); + } + } + } + + this.publish('error', err && err.reason ? err.reason : err); + }; + + /** + * Called when the transport disconnects. + * + * @api private + */ + + Socket.prototype.onDisconnect = function (reason) { + var wasConnected = this.connected + , wasConnecting = this.connecting; + + this.connected = false; + this.connecting = false; + this.open = false; + + if (wasConnected || wasConnecting) { + this.transport.close(); + this.transport.clearTimeouts(); + if (wasConnected) { + this.publish('disconnect', reason); + + if ('booted' != reason && this.options.reconnect && !this.reconnecting) { + this.reconnect(); + } + } + } + }; + + /** + * Called upon reconnection. + * + * @api private + */ + + Socket.prototype.reconnect = function () { + this.reconnecting = true; + this.reconnectionAttempts = 0; + this.reconnectionDelay = this.options['reconnection delay']; + + var self = this + , maxAttempts = this.options['max reconnection attempts'] + , tryMultiple = this.options['try multiple transports'] + , limit = this.options['reconnection limit']; + + function reset () { + if (self.connected) { + for (var i in self.namespaces) { + if (self.namespaces.hasOwnProperty(i) && '' !== i) { + self.namespaces[i].packet({ type: 'connect' }); + } + } + self.publish('reconnect', self.transport.name, self.reconnectionAttempts); + } + + clearTimeout(self.reconnectionTimer); + + self.removeListener('connect_failed', maybeReconnect); + self.removeListener('connect', maybeReconnect); + + self.reconnecting = false; + + delete self.reconnectionAttempts; + delete self.reconnectionDelay; + delete self.reconnectionTimer; + delete self.redoTransports; + + self.options['try multiple transports'] = tryMultiple; + }; + + function maybeReconnect () { + if (!self.reconnecting) { + return; + } + + if (self.connected) { + return reset(); + }; + + if (self.connecting && self.reconnecting) { + return self.reconnectionTimer = setTimeout(maybeReconnect, 1000); + } + + if (self.reconnectionAttempts++ >= maxAttempts) { + if (!self.redoTransports) { + self.on('connect_failed', maybeReconnect); + self.options['try multiple transports'] = true; + self.transports = self.origTransports; + self.transport = self.getTransport(); + self.redoTransports = true; + self.connect(); + } else { + self.publish('reconnect_failed'); + reset(); + } + } else { + if (self.reconnectionDelay < limit) { + self.reconnectionDelay *= 2; // exponential back off + } + + self.connect(); + self.publish('reconnecting', self.reconnectionDelay, self.reconnectionAttempts); + self.reconnectionTimer = setTimeout(maybeReconnect, self.reconnectionDelay); + } + }; + + this.options['try multiple transports'] = false; + this.reconnectionTimer = setTimeout(maybeReconnect, this.reconnectionDelay); + + this.on('connect', maybeReconnect); + }; + +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports + , this +); +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.SocketNamespace = SocketNamespace; + + /** + * Socket namespace constructor. + * + * @constructor + * @api public + */ + + function SocketNamespace (socket, name) { + this.socket = socket; + this.name = name || ''; + this.flags = {}; + this.json = new Flag(this, 'json'); + this.ackPackets = 0; + this.acks = {}; + }; + + /** + * Apply EventEmitter mixin. + */ + + io.util.mixin(SocketNamespace, io.EventEmitter); + + /** + * Copies emit since we override it + * + * @api private + */ + + SocketNamespace.prototype.$emit = io.EventEmitter.prototype.emit; + + /** + * Creates a new namespace, by proxying the request to the socket. This + * allows us to use the synax as we do on the server. + * + * @api public + */ + + SocketNamespace.prototype.of = function () { + return this.socket.of.apply(this.socket, arguments); + }; + + /** + * Sends a packet. + * + * @api private + */ + + SocketNamespace.prototype.packet = function (packet) { + packet.endpoint = this.name; + this.socket.packet(packet); + this.flags = {}; + return this; + }; + + /** + * Sends a message + * + * @api public + */ + + SocketNamespace.prototype.send = function (data, fn) { + var packet = { + type: this.flags.json ? 'json' : 'message' + , data: data + }; + + if ('function' == typeof fn) { + packet.id = ++this.ackPackets; + packet.ack = true; + this.acks[packet.id] = fn; + } + + return this.packet(packet); + }; + + /** + * Emits an event + * + * @api public + */ + + SocketNamespace.prototype.emit = function (name) { + var args = Array.prototype.slice.call(arguments, 1) + , lastArg = args[args.length - 1] + , packet = { + type: 'event' + , name: name + }; + + if ('function' == typeof lastArg) { + packet.id = ++this.ackPackets; + packet.ack = 'data'; + this.acks[packet.id] = lastArg; + args = args.slice(0, args.length - 1); + } + + packet.args = args; + + return this.packet(packet); + }; + + /** + * Disconnects the namespace + * + * @api private + */ + + SocketNamespace.prototype.disconnect = function () { + if (this.name === '') { + this.socket.disconnect(); + } else { + this.packet({ type: 'disconnect' }); + this.$emit('disconnect'); + } + + return this; + }; + + /** + * Handles a packet + * + * @api private + */ + + SocketNamespace.prototype.onPacket = function (packet) { + var self = this; + + function ack () { + self.packet({ + type: 'ack' + , args: io.util.toArray(arguments) + , ackId: packet.id + }); + }; + + switch (packet.type) { + case 'connect': + this.$emit('connect'); + break; + + case 'disconnect': + if (this.name === '') { + this.socket.onDisconnect(packet.reason || 'booted'); + } else { + this.$emit('disconnect', packet.reason); + } + break; + + case 'message': + case 'json': + var params = ['message', packet.data]; + + if (packet.ack == 'data') { + params.push(ack); + } else if (packet.ack) { + this.packet({ type: 'ack', ackId: packet.id }); + } + + this.$emit.apply(this, params); + break; + + case 'event': + var params = [packet.name].concat(packet.args); + + if (packet.ack == 'data') + params.push(ack); + + this.$emit.apply(this, params); + break; + + case 'ack': + if (this.acks[packet.ackId]) { + this.acks[packet.ackId].apply(this, packet.args); + delete this.acks[packet.ackId]; + } + break; + + case 'error': + if (packet.advice){ + this.socket.onError(packet); + } else { + if (packet.reason == 'unauthorized') { + this.$emit('connect_failed', packet.reason); + } else { + this.$emit('error', packet.reason); + } + } + break; + } + }; + + /** + * Flag interface. + * + * @api private + */ + + function Flag (nsp, name) { + this.namespace = nsp; + this.name = name; + }; + + /** + * Send a message + * + * @api public + */ + + Flag.prototype.send = function () { + this.namespace.flags[this.name] = true; + this.namespace.send.apply(this.namespace, arguments); + }; + + /** + * Emit an event + * + * @api public + */ + + Flag.prototype.emit = function () { + this.namespace.flags[this.name] = true; + this.namespace.emit.apply(this.namespace, arguments); + }; + +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io, global) { + + /** + * Expose constructor. + */ + + exports.websocket = WS; + + /** + * The WebSocket transport uses the HTML5 WebSocket API to establish an + * persistent connection with the Socket.IO server. This transport will also + * be inherited by the FlashSocket fallback as it provides a API compatible + * polyfill for the WebSockets. + * + * @constructor + * @extends {io.Transport} + * @api public + */ + + function WS (socket) { + io.Transport.apply(this, arguments); + }; + + /** + * Inherits from Transport. + */ + + io.util.inherit(WS, io.Transport); + + /** + * Transport name + * + * @api public + */ + + WS.prototype.name = 'websocket'; + + /** + * Initializes a new `WebSocket` connection with the Socket.IO server. We attach + * all the appropriate listeners to handle the responses from the server. + * + * @returns {Transport} + * @api public + */ + + WS.prototype.open = function () { + var query = io.util.query(this.socket.options.query) + , self = this + , Socket + + + if (!Socket) { + Socket = global.MozWebSocket || global.WebSocket; + } + + this.websocket = new Socket(this.prepareUrl() + query); + + this.websocket.onopen = function () { + self.onOpen(); + self.socket.setBuffer(false); + }; + this.websocket.onmessage = function (ev) { + self.onData(ev.data); + }; + this.websocket.onclose = function () { + self.onClose(); + self.socket.setBuffer(true); + }; + this.websocket.onerror = function (e) { + self.onError(e); + }; + + return this; + }; + + /** + * Send a message to the Socket.IO server. The message will automatically be + * encoded in the correct message format. + * + * @returns {Transport} + * @api public + */ + + // Do to a bug in the current IDevices browser, we need to wrap the send in a + // setTimeout, when they resume from sleeping the browser will crash if + // we don't allow the browser time to detect the socket has been closed + if (io.util.ua.iDevice) { + WS.prototype.send = function (data) { + var self = this; + setTimeout(function() { + self.websocket.send(data); + },0); + return this; + }; + } else { + WS.prototype.send = function (data) { + this.websocket.send(data); + return this; + }; + } + + /** + * Payload + * + * @api private + */ + + WS.prototype.payload = function (arr) { + for (var i = 0, l = arr.length; i < l; i++) { + this.packet(arr[i]); + } + return this; + }; + + /** + * Disconnect the established `WebSocket` connection. + * + * @returns {Transport} + * @api public + */ + + WS.prototype.close = function () { + this.websocket.close(); + return this; + }; + + /** + * Handle the errors that `WebSocket` might be giving when we + * are attempting to connect or send messages. + * + * @param {Error} e The error. + * @api private + */ + + WS.prototype.onError = function (e) { + this.socket.onError(e); + }; + + /** + * Returns the appropriate scheme for the URI generation. + * + * @api private + */ + WS.prototype.scheme = function () { + return this.socket.options.secure ? 'wss' : 'ws'; + }; + + /** + * Checks if the browser has support for native `WebSockets` and that + * it's not the polyfill created for the FlashSocket transport. + * + * @return {Boolean} + * @api public + */ + + WS.check = function () { + return ('WebSocket' in global && !('__addTask' in WebSocket)) + || 'MozWebSocket' in global; + }; + + /** + * Check if the `WebSocket` transport support cross domain communications. + * + * @returns {Boolean} + * @api public + */ + + WS.xdomainCheck = function () { + return true; + }; + + /** + * Add the transport to your public io.transports array. + * + * @api private + */ + + io.transports.push('websocket'); + +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports + , this +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.flashsocket = Flashsocket; + + /** + * The FlashSocket transport. This is a API wrapper for the HTML5 WebSocket + * specification. It uses a .swf file to communicate with the server. If you want + * to serve the .swf file from a other server than where the Socket.IO script is + * coming from you need to use the insecure version of the .swf. More information + * about this can be found on the github page. + * + * @constructor + * @extends {io.Transport.websocket} + * @api public + */ + + function Flashsocket () { + io.Transport.websocket.apply(this, arguments); + }; + + /** + * Inherits from Transport. + */ + + io.util.inherit(Flashsocket, io.Transport.websocket); + + /** + * Transport name + * + * @api public + */ + + Flashsocket.prototype.name = 'flashsocket'; + + /** + * Disconnect the established `FlashSocket` connection. This is done by adding a + * new task to the FlashSocket. The rest will be handled off by the `WebSocket` + * transport. + * + * @returns {Transport} + * @api public + */ + + Flashsocket.prototype.open = function () { + var self = this + , args = arguments; + + WebSocket.__addTask(function () { + io.Transport.websocket.prototype.open.apply(self, args); + }); + return this; + }; + + /** + * Sends a message to the Socket.IO server. This is done by adding a new + * task to the FlashSocket. The rest will be handled off by the `WebSocket` + * transport. + * + * @returns {Transport} + * @api public + */ + + Flashsocket.prototype.send = function () { + var self = this, args = arguments; + WebSocket.__addTask(function () { + io.Transport.websocket.prototype.send.apply(self, args); + }); + return this; + }; + + /** + * Disconnects the established `FlashSocket` connection. + * + * @returns {Transport} + * @api public + */ + + Flashsocket.prototype.close = function () { + WebSocket.__tasks.length = 0; + io.Transport.websocket.prototype.close.call(this); + return this; + }; + + /** + * The WebSocket fall back needs to append the flash container to the body + * element, so we need to make sure we have access to it. Or defer the call + * until we are sure there is a body element. + * + * @param {Socket} socket The socket instance that needs a transport + * @param {Function} fn The callback + * @api private + */ + + Flashsocket.prototype.ready = function (socket, fn) { + function init () { + var options = socket.options + , port = options['flash policy port'] + , path = [ + 'http' + (options.secure ? 's' : '') + ':/' + , options.host + ':' + options.port + , options.resource + , 'static/flashsocket' + , 'WebSocketMain' + (socket.isXDomain() ? 'Insecure' : '') + '.swf' + ]; + + // Only start downloading the swf file when the checked that this browser + // actually supports it + if (!Flashsocket.loaded) { + if (typeof WEB_SOCKET_SWF_LOCATION === 'undefined') { + // Set the correct file based on the XDomain settings + WEB_SOCKET_SWF_LOCATION = path.join('/'); + } + + if (port !== 843) { + WebSocket.loadFlashPolicyFile('xmlsocket://' + options.host + ':' + port); + } + + WebSocket.__initialize(); + Flashsocket.loaded = true; + } + + fn.call(self); + } + + var self = this; + if (document.body) return init(); + + io.util.load(init); + }; + + /** + * Check if the FlashSocket transport is supported as it requires that the Adobe + * Flash Player plug-in version `10.0.0` or greater is installed. And also check if + * the polyfill is correctly loaded. + * + * @returns {Boolean} + * @api public + */ + + Flashsocket.check = function () { + if ( + typeof WebSocket == 'undefined' + || !('__initialize' in WebSocket) || !swfobject + ) return false; + + return swfobject.getFlashPlayerVersion().major >= 10; + }; + + /** + * Check if the FlashSocket transport can be used as cross domain / cross origin + * transport. Because we can't see which type (secure or insecure) of .swf is used + * we will just return true. + * + * @returns {Boolean} + * @api public + */ + + Flashsocket.xdomainCheck = function () { + return true; + }; + + /** + * Disable AUTO_INITIALIZATION + */ + + if (typeof window != 'undefined') { + WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true; + } + + /** + * Add the transport to your public io.transports array. + * + * @api private + */ + + io.transports.push('flashsocket'); +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); +/* SWFObject v2.2 + is released under the MIT License +*/ +if ('undefined' != typeof window) { +var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O[(['Active'].concat('Object').join('X'))]!=D){try{var ad=new window[(['Active'].concat('Object').join('X'))](W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='"+af+"";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab +// License: New BSD License +// Reference: http://dev.w3.org/html5/websockets/ +// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol + +(function() { + + if ('undefined' == typeof window || window.WebSocket) return; + + var console = window.console; + if (!console || !console.log || !console.error) { + console = {log: function(){ }, error: function(){ }}; + } + + if (!swfobject.hasFlashPlayerVersion("10.0.0")) { + console.error("Flash Player >= 10.0.0 is required."); + return; + } + if (location.protocol == "file:") { + console.error( + "WARNING: web-socket-js doesn't work in file:///... URL " + + "unless you set Flash Security Settings properly. " + + "Open the page via Web server i.e. http://..."); + } + + /** + * This class represents a faux web socket. + * @param {string} url + * @param {array or string} protocols + * @param {string} proxyHost + * @param {int} proxyPort + * @param {string} headers + */ + WebSocket = function(url, protocols, proxyHost, proxyPort, headers) { + var self = this; + self.__id = WebSocket.__nextId++; + WebSocket.__instances[self.__id] = self; + self.readyState = WebSocket.CONNECTING; + self.bufferedAmount = 0; + self.__events = {}; + if (!protocols) { + protocols = []; + } else if (typeof protocols == "string") { + protocols = [protocols]; + } + // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. + // Otherwise, when onopen fires immediately, onopen is called before it is set. + setTimeout(function() { + WebSocket.__addTask(function() { + WebSocket.__flash.create( + self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null); + }); + }, 0); + }; + + /** + * Send data to the web socket. + * @param {string} data The data to send to the socket. + * @return {boolean} True for success, false for failure. + */ + WebSocket.prototype.send = function(data) { + if (this.readyState == WebSocket.CONNECTING) { + throw "INVALID_STATE_ERR: Web Socket connection has not been established"; + } + // We use encodeURIComponent() here, because FABridge doesn't work if + // the argument includes some characters. We don't use escape() here + // because of this: + // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions + // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't + // preserve all Unicode characters either e.g. "\uffff" in Firefox. + // Note by wtritch: Hopefully this will not be necessary using ExternalInterface. Will require + // additional testing. + var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data)); + if (result < 0) { // success + return true; + } else { + this.bufferedAmount += result; + return false; + } + }; + + /** + * Close this web socket gracefully. + */ + WebSocket.prototype.close = function() { + if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) { + return; + } + this.readyState = WebSocket.CLOSING; + WebSocket.__flash.close(this.__id); + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture + * @return void + */ + WebSocket.prototype.addEventListener = function(type, listener, useCapture) { + if (!(type in this.__events)) { + this.__events[type] = []; + } + this.__events[type].push(listener); + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture + * @return void + */ + WebSocket.prototype.removeEventListener = function(type, listener, useCapture) { + if (!(type in this.__events)) return; + var events = this.__events[type]; + for (var i = events.length - 1; i >= 0; --i) { + if (events[i] === listener) { + events.splice(i, 1); + break; + } + } + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {Event} event + * @return void + */ + WebSocket.prototype.dispatchEvent = function(event) { + var events = this.__events[event.type] || []; + for (var i = 0; i < events.length; ++i) { + events[i](event); + } + var handler = this["on" + event.type]; + if (handler) handler(event); + }; + + /** + * Handles an event from Flash. + * @param {Object} flashEvent + */ + WebSocket.prototype.__handleEvent = function(flashEvent) { + if ("readyState" in flashEvent) { + this.readyState = flashEvent.readyState; + } + if ("protocol" in flashEvent) { + this.protocol = flashEvent.protocol; + } + + var jsEvent; + if (flashEvent.type == "open" || flashEvent.type == "error") { + jsEvent = this.__createSimpleEvent(flashEvent.type); + } else if (flashEvent.type == "close") { + // TODO implement jsEvent.wasClean + jsEvent = this.__createSimpleEvent("close"); + } else if (flashEvent.type == "message") { + var data = decodeURIComponent(flashEvent.message); + jsEvent = this.__createMessageEvent("message", data); + } else { + throw "unknown event type: " + flashEvent.type; + } + + this.dispatchEvent(jsEvent); + }; + + WebSocket.prototype.__createSimpleEvent = function(type) { + if (document.createEvent && window.Event) { + var event = document.createEvent("Event"); + event.initEvent(type, false, false); + return event; + } else { + return {type: type, bubbles: false, cancelable: false}; + } + }; + + WebSocket.prototype.__createMessageEvent = function(type, data) { + if (document.createEvent && window.MessageEvent && !window.opera) { + var event = document.createEvent("MessageEvent"); + event.initMessageEvent("message", false, false, data, null, null, window, null); + return event; + } else { + // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes. + return {type: type, data: data, bubbles: false, cancelable: false}; + } + }; + + /** + * Define the WebSocket readyState enumeration. + */ + WebSocket.CONNECTING = 0; + WebSocket.OPEN = 1; + WebSocket.CLOSING = 2; + WebSocket.CLOSED = 3; + + WebSocket.__flash = null; + WebSocket.__instances = {}; + WebSocket.__tasks = []; + WebSocket.__nextId = 0; + + /** + * Load a new flash security policy file. + * @param {string} url + */ + WebSocket.loadFlashPolicyFile = function(url){ + WebSocket.__addTask(function() { + WebSocket.__flash.loadManualPolicyFile(url); + }); + }; + + /** + * Loads WebSocketMain.swf and creates WebSocketMain object in Flash. + */ + WebSocket.__initialize = function() { + if (WebSocket.__flash) return; + + if (WebSocket.__swfLocation) { + // For backword compatibility. + window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; + } + if (!window.WEB_SOCKET_SWF_LOCATION) { + console.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); + return; + } + var container = document.createElement("div"); + container.id = "webSocketContainer"; + // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents + // Flash from loading at least in IE. So we move it out of the screen at (-100, -100). + // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash + // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is + // the best we can do as far as we know now. + container.style.position = "absolute"; + if (WebSocket.__isFlashLite()) { + container.style.left = "0px"; + container.style.top = "0px"; + } else { + container.style.left = "-100px"; + container.style.top = "-100px"; + } + var holder = document.createElement("div"); + holder.id = "webSocketFlash"; + container.appendChild(holder); + document.body.appendChild(container); + // See this article for hasPriority: + // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html + swfobject.embedSWF( + WEB_SOCKET_SWF_LOCATION, + "webSocketFlash", + "1" /* width */, + "1" /* height */, + "10.0.0" /* SWF version */, + null, + null, + {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"}, + null, + function(e) { + if (!e.success) { + console.error("[WebSocket] swfobject.embedSWF failed"); + } + }); + }; + + /** + * Called by Flash to notify JS that it's fully loaded and ready + * for communication. + */ + WebSocket.__onFlashInitialized = function() { + // We need to set a timeout here to avoid round-trip calls + // to flash during the initialization process. + setTimeout(function() { + WebSocket.__flash = document.getElementById("webSocketFlash"); + WebSocket.__flash.setCallerUrl(location.href); + WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG); + for (var i = 0; i < WebSocket.__tasks.length; ++i) { + WebSocket.__tasks[i](); + } + WebSocket.__tasks = []; + }, 0); + }; + + /** + * Called by Flash to notify WebSockets events are fired. + */ + WebSocket.__onFlashEvent = function() { + setTimeout(function() { + try { + // Gets events using receiveEvents() instead of getting it from event object + // of Flash event. This is to make sure to keep message order. + // It seems sometimes Flash events don't arrive in the same order as they are sent. + var events = WebSocket.__flash.receiveEvents(); + for (var i = 0; i < events.length; ++i) { + WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]); + } + } catch (e) { + console.error(e); + } + }, 0); + return true; + }; + + // Called by Flash. + WebSocket.__log = function(message) { + console.log(decodeURIComponent(message)); + }; + + // Called by Flash. + WebSocket.__error = function(message) { + console.error(decodeURIComponent(message)); + }; + + WebSocket.__addTask = function(task) { + if (WebSocket.__flash) { + task(); + } else { + WebSocket.__tasks.push(task); + } + }; + + /** + * Test if the browser is running flash lite. + * @return {boolean} True if flash lite is running, false otherwise. + */ + WebSocket.__isFlashLite = function() { + if (!window.navigator || !window.navigator.mimeTypes) { + return false; + } + var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"]; + if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) { + return false; + } + return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false; + }; + + if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { + if (window.addEventListener) { + window.addEventListener("load", function(){ + WebSocket.__initialize(); + }, false); + } else { + window.attachEvent("onload", function(){ + WebSocket.__initialize(); + }); + } + } + +})(); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io, global) { + + /** + * Expose constructor. + * + * @api public + */ + + exports.XHR = XHR; + + /** + * XHR constructor + * + * @costructor + * @api public + */ + + function XHR (socket) { + if (!socket) return; + + io.Transport.apply(this, arguments); + this.sendBuffer = []; + }; + + /** + * Inherits from Transport. + */ + + io.util.inherit(XHR, io.Transport); + + /** + * Establish a connection + * + * @returns {Transport} + * @api public + */ + + XHR.prototype.open = function () { + this.socket.setBuffer(false); + this.onOpen(); + this.get(); + + // we need to make sure the request succeeds since we have no indication + // whether the request opened or not until it succeeded. + this.setCloseTimeout(); + + return this; + }; + + /** + * Check if we need to send data to the Socket.IO server, if we have data in our + * buffer we encode it and forward it to the `post` method. + * + * @api private + */ + + XHR.prototype.payload = function (payload) { + var msgs = []; + + for (var i = 0, l = payload.length; i < l; i++) { + msgs.push(io.parser.encodePacket(payload[i])); + } + + this.send(io.parser.encodePayload(msgs)); + }; + + /** + * Send data to the Socket.IO server. + * + * @param data The message + * @returns {Transport} + * @api public + */ + + XHR.prototype.send = function (data) { + this.post(data); + return this; + }; + + /** + * Posts a encoded message to the Socket.IO server. + * + * @param {String} data A encoded message. + * @api private + */ + + function empty () { }; + + XHR.prototype.post = function (data) { + var self = this; + this.socket.setBuffer(true); + + function stateChange () { + if (this.readyState == 4) { + this.onreadystatechange = empty; + self.posting = false; + + if (this.status == 200){ + self.socket.setBuffer(false); + } else { + self.onClose(); + } + } + } + + function onload () { + this.onload = empty; + self.socket.setBuffer(false); + }; + + this.sendXHR = this.request('POST'); + + if (global.XDomainRequest && this.sendXHR instanceof XDomainRequest) { + this.sendXHR.onload = this.sendXHR.onerror = onload; + } else { + this.sendXHR.onreadystatechange = stateChange; + } + + this.sendXHR.send(data); + }; + + /** + * Disconnects the established `XHR` connection. + * + * @returns {Transport} + * @api public + */ + + XHR.prototype.close = function () { + this.onClose(); + return this; + }; + + /** + * Generates a configured XHR request + * + * @param {String} url The url that needs to be requested. + * @param {String} method The method the request should use. + * @returns {XMLHttpRequest} + * @api private + */ + + XHR.prototype.request = function (method) { + var req = io.util.request(this.socket.isXDomain()) + , query = io.util.query(this.socket.options.query, 't=' + +new Date); + + req.open(method || 'GET', this.prepareUrl() + query, true); + + if (method == 'POST') { + try { + if (req.setRequestHeader) { + req.setRequestHeader('Content-type', 'text/plain;charset=UTF-8'); + } else { + // XDomainRequest + req.contentType = 'text/plain'; + } + } catch (e) {} + } + + return req; + }; + + /** + * Returns the scheme to use for the transport URLs. + * + * @api private + */ + + XHR.prototype.scheme = function () { + return this.socket.options.secure ? 'https' : 'http'; + }; + + /** + * Check if the XHR transports are supported + * + * @param {Boolean} xdomain Check if we support cross domain requests. + * @returns {Boolean} + * @api public + */ + + XHR.check = function (socket, xdomain) { + try { + var request = io.util.request(xdomain), + usesXDomReq = (global.XDomainRequest && request instanceof XDomainRequest), + socketProtocol = (socket && socket.options && socket.options.secure ? 'https:' : 'http:'), + isXProtocol = (global.location && socketProtocol != global.location.protocol); + if (request && !(usesXDomReq && isXProtocol)) { + return true; + } + } catch(e) {} + + return false; + }; + + /** + * Check if the XHR transport supports cross domain requests. + * + * @returns {Boolean} + * @api public + */ + + XHR.xdomainCheck = function (socket) { + return XHR.check(socket, true); + }; + +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports + , this +); +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.htmlfile = HTMLFile; + + /** + * The HTMLFile transport creates a `forever iframe` based transport + * for Internet Explorer. Regular forever iframe implementations will + * continuously trigger the browsers buzy indicators. If the forever iframe + * is created inside a `htmlfile` these indicators will not be trigged. + * + * @constructor + * @extends {io.Transport.XHR} + * @api public + */ + + function HTMLFile (socket) { + io.Transport.XHR.apply(this, arguments); + }; + + /** + * Inherits from XHR transport. + */ + + io.util.inherit(HTMLFile, io.Transport.XHR); + + /** + * Transport name + * + * @api public + */ + + HTMLFile.prototype.name = 'htmlfile'; + + /** + * Creates a new Ac...eX `htmlfile` with a forever loading iframe + * that can be used to listen to messages. Inside the generated + * `htmlfile` a reference will be made to the HTMLFile transport. + * + * @api private + */ + + HTMLFile.prototype.get = function () { + this.doc = new window[(['Active'].concat('Object').join('X'))]('htmlfile'); + this.doc.open(); + this.doc.write(''); + this.doc.close(); + this.doc.parentWindow.s = this; + + var iframeC = this.doc.createElement('div'); + iframeC.className = 'socketio'; + + this.doc.body.appendChild(iframeC); + this.iframe = this.doc.createElement('iframe'); + + iframeC.appendChild(this.iframe); + + var self = this + , query = io.util.query(this.socket.options.query, 't='+ +new Date); + + this.iframe.src = this.prepareUrl() + query; + + io.util.on(window, 'unload', function () { + self.destroy(); + }); + }; + + /** + * The Socket.IO server will write script tags inside the forever + * iframe, this function will be used as callback for the incoming + * information. + * + * @param {String} data The message + * @param {document} doc Reference to the context + * @api private + */ + + HTMLFile.prototype._ = function (data, doc) { + // unescape all forward slashes. see GH-1251 + data = data.replace(/\\\//g, '/'); + this.onData(data); + try { + var script = doc.getElementsByTagName('script')[0]; + script.parentNode.removeChild(script); + } catch (e) { } + }; + + /** + * Destroy the established connection, iframe and `htmlfile`. + * And calls the `CollectGarbage` function of Internet Explorer + * to release the memory. + * + * @api private + */ + + HTMLFile.prototype.destroy = function () { + if (this.iframe){ + try { + this.iframe.src = 'about:blank'; + } catch(e){} + + this.doc = null; + this.iframe.parentNode.removeChild(this.iframe); + this.iframe = null; + + CollectGarbage(); + } + }; + + /** + * Disconnects the established connection. + * + * @returns {Transport} Chaining. + * @api public + */ + + HTMLFile.prototype.close = function () { + this.destroy(); + return io.Transport.XHR.prototype.close.call(this); + }; + + /** + * Checks if the browser supports this transport. The browser + * must have an `Ac...eXObject` implementation. + * + * @return {Boolean} + * @api public + */ + + HTMLFile.check = function (socket) { + if (typeof window != "undefined" && (['Active'].concat('Object').join('X')) in window){ + try { + var a = new window[(['Active'].concat('Object').join('X'))]('htmlfile'); + return a && io.Transport.XHR.check(socket); + } catch(e){} + } + return false; + }; + + /** + * Check if cross domain requests are supported. + * + * @returns {Boolean} + * @api public + */ + + HTMLFile.xdomainCheck = function () { + // we can probably do handling for sub-domains, we should + // test that it's cross domain but a subdomain here + return false; + }; + + /** + * Add the transport to your public io.transports array. + * + * @api private + */ + + io.transports.push('htmlfile'); + +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io, global) { + + /** + * Expose constructor. + */ + + exports['xhr-polling'] = XHRPolling; + + /** + * The XHR-polling transport uses long polling XHR requests to create a + * "persistent" connection with the server. + * + * @constructor + * @api public + */ + + function XHRPolling () { + io.Transport.XHR.apply(this, arguments); + }; + + /** + * Inherits from XHR transport. + */ + + io.util.inherit(XHRPolling, io.Transport.XHR); + + /** + * Merge the properties from XHR transport + */ + + io.util.merge(XHRPolling, io.Transport.XHR); + + /** + * Transport name + * + * @api public + */ + + XHRPolling.prototype.name = 'xhr-polling'; + + /** + * Indicates whether heartbeats is enabled for this transport + * + * @api private + */ + + XHRPolling.prototype.heartbeats = function () { + return false; + }; + + /** + * Establish a connection, for iPhone and Android this will be done once the page + * is loaded. + * + * @returns {Transport} Chaining. + * @api public + */ + + XHRPolling.prototype.open = function () { + var self = this; + + io.Transport.XHR.prototype.open.call(self); + return false; + }; + + /** + * Starts a XHR request to wait for incoming messages. + * + * @api private + */ + + function empty () {}; + + XHRPolling.prototype.get = function () { + if (!this.isOpen) return; + + var self = this; + + function stateChange () { + if (this.readyState == 4) { + this.onreadystatechange = empty; + + if (this.status == 200) { + self.onData(this.responseText); + self.get(); + } else { + self.onClose(); + } + } + }; + + function onload () { + this.onload = empty; + this.onerror = empty; + self.retryCounter = 1; + self.onData(this.responseText); + self.get(); + }; + + function onerror () { + self.retryCounter ++; + if(!self.retryCounter || self.retryCounter > 3) { + self.onClose(); + } else { + self.get(); + } + }; + + this.xhr = this.request(); + + if (global.XDomainRequest && this.xhr instanceof XDomainRequest) { + this.xhr.onload = onload; + this.xhr.onerror = onerror; + } else { + this.xhr.onreadystatechange = stateChange; + } + + this.xhr.send(null); + }; + + /** + * Handle the unclean close behavior. + * + * @api private + */ + + XHRPolling.prototype.onClose = function () { + io.Transport.XHR.prototype.onClose.call(this); + + if (this.xhr) { + this.xhr.onreadystatechange = this.xhr.onload = this.xhr.onerror = empty; + try { + this.xhr.abort(); + } catch(e){} + this.xhr = null; + } + }; + + /** + * Webkit based browsers show a infinit spinner when you start a XHR request + * before the browsers onload event is called so we need to defer opening of + * the transport until the onload event is called. Wrapping the cb in our + * defer method solve this. + * + * @param {Socket} socket The socket instance that needs a transport + * @param {Function} fn The callback + * @api private + */ + + XHRPolling.prototype.ready = function (socket, fn) { + var self = this; + + io.util.defer(function () { + fn.call(self); + }); + }; + + /** + * Add the transport to your public io.transports array. + * + * @api private + */ + + io.transports.push('xhr-polling'); + +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports + , this +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io, global) { + /** + * There is a way to hide the loading indicator in Firefox. If you create and + * remove a iframe it will stop showing the current loading indicator. + * Unfortunately we can't feature detect that and UA sniffing is evil. + * + * @api private + */ + + var indicator = global.document && "MozAppearance" in + global.document.documentElement.style; + + /** + * Expose constructor. + */ + + exports['jsonp-polling'] = JSONPPolling; + + /** + * The JSONP transport creates an persistent connection by dynamically + * inserting a script tag in the page. This script tag will receive the + * information of the Socket.IO server. When new information is received + * it creates a new script tag for the new data stream. + * + * @constructor + * @extends {io.Transport.xhr-polling} + * @api public + */ + + function JSONPPolling (socket) { + io.Transport['xhr-polling'].apply(this, arguments); + + this.index = io.j.length; + + var self = this; + + io.j.push(function (msg) { + self._(msg); + }); + }; + + /** + * Inherits from XHR polling transport. + */ + + io.util.inherit(JSONPPolling, io.Transport['xhr-polling']); + + /** + * Transport name + * + * @api public + */ + + JSONPPolling.prototype.name = 'jsonp-polling'; + + /** + * Posts a encoded message to the Socket.IO server using an iframe. + * The iframe is used because script tags can create POST based requests. + * The iframe is positioned outside of the view so the user does not + * notice it's existence. + * + * @param {String} data A encoded message. + * @api private + */ + + JSONPPolling.prototype.post = function (data) { + var self = this + , query = io.util.query( + this.socket.options.query + , 't='+ (+new Date) + '&i=' + this.index + ); + + if (!this.form) { + var form = document.createElement('form') + , area = document.createElement('textarea') + , id = this.iframeId = 'socketio_iframe_' + this.index + , iframe; + + form.className = 'socketio'; + form.style.position = 'absolute'; + form.style.top = '0px'; + form.style.left = '0px'; + form.style.display = 'none'; + form.target = id; + form.method = 'POST'; + form.setAttribute('accept-charset', 'utf-8'); + area.name = 'd'; + form.appendChild(area); + document.body.appendChild(form); + + this.form = form; + this.area = area; + } + + this.form.action = this.prepareUrl() + query; + + function complete () { + initIframe(); + self.socket.setBuffer(false); + }; + + function initIframe () { + if (self.iframe) { + self.form.removeChild(self.iframe); + } + + try { + // ie6 dynamic iframes with target="" support (thanks Chris Lambacher) + iframe = document.createElement(' -

        -

        Who finds it useful?

        -

        Designers, inventors, artists, educators, strategists, consultants, facilitators, entrepreneurs, systems thinkers, changemakers, analysts, students, researchers... maybe you!

        - - EXPLORE FEATURED MAPS - REQUEST INVITE -
        -
        -
        -
        -
        -
        - <% # our partners %> -
        -
        -
    - - -<% elsif authenticated? %> - <% content_for :title, "Explore Active Maps | Metamaps" %> - -<% end %> + # +%> + +<% content_for :title, "Home | Metamaps" %> +
    +
    +
    Make Sense with Metamaps
    +
    + METAMAPS.CC is a free and open source platform that supports real-time sense-making, distributed collaboration, and the creative intelligence of individuals, organizations and communities. We are currently in an invite-only beta. +
    +
    +
    +
    + +
    +

    Who finds it useful?

    +

    Designers, inventors, artists, educators, strategists, consultants, facilitators, entrepreneurs, systems thinkers, changemakers, analysts, students, researchers... maybe you!

    + + EXPLORE FEATURED MAPS + REQUEST INVITE +
    +
    +
    +
    +
    +
    + <% # our partners %> +
    +
    +
    + + From d50923b6bd789df3db812dc75b5081489ad8c2f1 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 29 Feb 2016 17:05:11 +1300 Subject: [PATCH 199/305] fix js bug --- app/assets/javascripts/src/Metamaps.GlobalUI.js.erb | 1 + app/assets/javascripts/src/Metamaps.js.erb | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb index a7f2f017..b8c111af 100644 --- a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb +++ b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb @@ -48,6 +48,7 @@ $(document).ready(function () { // this runs the init function within each sub-object on the Metamaps one if (Metamaps.hasOwnProperty(prop) && + Metamaps[prop] != null && Metamaps[prop].hasOwnProperty('init') && typeof (Metamaps[prop].init) == 'function' ) { diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index 4744105c..c0c7e858 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -17,12 +17,10 @@ var labelType, useGradients, nativeTextSupport, animate; })(); // TODO eliminate these 4 top-level variables -Metamaps = { - panningInt: null, - tempNode: null, - tempInit: false, - tempNode2: null -} +Metamaps.panningInt = null; +Metamaps.tempNode = null; +Metamaps.tempInit = false; +Metamaps.tempNode2 = null; Metamaps.Settings = { embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages From 4e92d4c2c5fc20dc8284f749965e71ab73ac95db Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 29 Feb 2016 17:05:36 +1300 Subject: [PATCH 200/305] change video border width --- app/assets/javascripts/src/Metamaps.js.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index c0c7e858..e5c00ffd 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -2050,7 +2050,7 @@ Metamaps.Realtime = { left: left + 'px' }); v.$container.find('.video-cutoff').css({ - border: '5px solid ' + self.mappersOnMap[id].color + border: '4px solid ' + self.mappersOnMap[id].color }); self.videosInPosition += 1; }, @@ -2118,7 +2118,7 @@ Metamaps.Realtime = { self: true }; self.localVideo.view.$container.find('.video-cutoff').css({ - border: '5px solid ' + self.activeMapper.color + border: '4px solid ' + self.activeMapper.color }); self.room.chat.addParticipant(self.activeMapper); }, From bd5c88fb0ba06135b481691c4312f10d3896d59f Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Tue, 1 Mar 2016 12:25:38 +1300 Subject: [PATCH 201/305] display initial state of call to new mapper --- app/assets/javascripts/src/Metamaps.js.erb | 40 +++++++++++-------- .../javascripts/src/views/chatView.js.erb | 1 + app/assets/stylesheets/junto.css.erb | 4 +- npm-debug.log | 19 +++++++++ realtime/realtime-server.js | 6 ++- 5 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 npm-debug.log diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index e5c00ffd..08eb6cfe 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -2126,7 +2126,7 @@ Metamaps.Realtime = { var self = Metamaps.Realtime; self.socket.emit('checkForCall', { room: self.room.room, mapid: Metamaps.Active.Map.id }); }, - promptToJoin: function (data) { + promptToJoin: function () { var self = Metamaps.Realtime; var notifyText = 'There\'s a conversation happening, want to join?'; @@ -2224,7 +2224,7 @@ Metamaps.Realtime = { var username = self.mappersOnMap[inviter].name; var notifyText = username + ' is inviting you to the conversation. Join?'; notifyText += ' '; - notifyText += ' '; + notifyText += ' '; Metamaps.GlobalUI.notifyUser(notifyText, true); }, acceptCall: function (userid) { @@ -2246,6 +2246,15 @@ Metamaps.Realtime = { }); Metamaps.GlobalUI.clearNotify(); }, + denyInvite: function (userid) { + var self = Metamaps.Realtime; + self.socket.emit('inviteDenied', { + mapid: Metamaps.Active.Map.id, + invited: Metamaps.Active.Mapper.id, + inviter: userid + }); + Metamaps.GlobalUI.clearNotify(); + }, inviteACall: function (userid) { var self = Metamaps.Realtime; self.socket.emit('inviteACall', { @@ -2263,7 +2272,7 @@ Metamaps.Realtime = { inviter: Metamaps.Active.Mapper.id, invited: userid }); - Metamaps.GlobalUI.notifyUser('Invitation has been sent.'); + self.room.chat.invitationPending(userid); }, callAccepted: function (userid) { var self = Metamaps.Realtime; @@ -2280,6 +2289,13 @@ Metamaps.Realtime = { Metamaps.GlobalUI.notifyUser(username + ' didn\'t accept your invite.'); self.room.chat.invitationAnswered(userid); }, + inviteDenied: function (userid) { + var self = Metamaps.Realtime; + + var username = self.mappersOnMap[userid].name; + Metamaps.GlobalUI.notifyUser(username + ' didn\'t accept your invite.'); + self.room.chat.invitationAnswered(userid); + }, joinCall: function () { var self = Metamaps.Realtime; @@ -2352,6 +2368,7 @@ Metamaps.Realtime = { socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToJoin', self.invitedToJoin); // call already in progress socket.on(myId + '-' + Metamaps.Active.Map.id + '-callAccepted', self.callAccepted); socket.on(myId + '-' + Metamaps.Active.Map.id + '-callDenied', self.callDenied); + socket.on(myId + '-' + Metamaps.Active.Map.id + '-inviteDenied', self.inviteDenied); // receive word that there's a conversation in progress socket.on('maps-' + Metamaps.Active.Map.id + '-callInProgress', self.promptToJoin); @@ -2520,24 +2537,12 @@ Metamaps.Realtime = { coords: { x: 0, y: 0 - }, + } }; - var onOff = data.userrealtime ? "On" : "Off"; - var mapperListItem = '
  • '; - mapperListItem += ''; - mapperListItem += data.username; - mapperListItem += '
    '; - mapperListItem += '
  • '; - if (data.userid !== Metamaps.Active.Mapper.id) { - //$('#mapper' + data.userid).remove(); self.room.chat.addParticipant(self.mappersOnMap[data.userid]); + if (data.userinconversation) self.room.chat.mapperJoinedCall(data.userid); // create a div for the collaborators compass self.createCompass(data.username, data.userid, data.userimage, self.mappersOnMap[data.userid].color, !self.status); @@ -2586,6 +2591,7 @@ Metamaps.Realtime = { userimage: Metamaps.Active.Mapper.get("image"), userid: Metamaps.Active.Mapper.id, userrealtime: self.status, + userinconversation: self.inConversation, mapid: Metamaps.Active.Map.id }; socket.emit('updateNewMapperList', update); diff --git a/app/assets/javascripts/src/views/chatView.js.erb b/app/assets/javascripts/src/views/chatView.js.erb index 064cc557..0ba86ee6 100644 --- a/app/assets/javascripts/src/views/chatView.js.erb +++ b/app/assets/javascripts/src/views/chatView.js.erb @@ -235,6 +235,7 @@ Metamaps.Views.chatView = (function () { this.$participants.removeClass('is-participating'); this.$button.removeClass('active'); this.$participants.find('.participant').removeClass('active'); + this.$participants.find('.participant').removeClass('pending'); } chatView.prototype.leaveConversation = function () { diff --git a/app/assets/stylesheets/junto.css.erb b/app/assets/stylesheets/junto.css.erb index 7606532f..ed9b62b5 100644 --- a/app/assets/stylesheets/junto.css.erb +++ b/app/assets/stylesheets/junto.css.erb @@ -1,5 +1,5 @@ .collaborator-video { - z-index: 2; + z-index: 1; position: absolute; width: 150px; height: 150px; @@ -236,7 +236,7 @@ .chat-box .participants .participant .chat-participant-invite-join { display: none; } -.chat-box .participants.is-live .participant:not(.active) .chat-participant-invite-join { +.chat-box .participants.is-live.is-participating .participant:not(.active) .chat-participant-invite-join { display: block; } .chat-box .participants .participant .chat-participant-invite-call, diff --git a/npm-debug.log b/npm-debug.log new file mode 100644 index 00000000..021be352 --- /dev/null +++ b/npm-debug.log @@ -0,0 +1,19 @@ +0 info it worked if it ends with ok +1 verbose cli [ '/usr/bin/nodejs', '/usr/bin/npm', 'install' ] +2 info using npm@1.3.10 +3 info using node@v0.10.25 +4 error install Couldn't read dependencies +5 error Error: ENOENT, open '/vagrant/package.json' +6 error If you need help, you may report this log at: +6 error +6 error or email it to: +6 error +7 error System Linux 3.13.0-66-generic +8 error command "/usr/bin/nodejs" "/usr/bin/npm" "install" +9 error cwd /vagrant +10 error node -v v0.10.25 +11 error npm -v 1.3.10 +12 error path /vagrant/package.json +13 error code ENOENT +14 error errno 34 +15 verbose exit [ 34, true ] diff --git a/realtime/realtime-server.js b/realtime/realtime-server.js index 9326dd4d..6ce9aa44 100644 --- a/realtime/realtime-server.js +++ b/realtime/realtime-server.js @@ -17,6 +17,7 @@ function start() { userid: data.userid, username: data.username, userrealtime: data.userrealtime, + userinconversation: data.userinconversation, userimage: data.userimage }; socket.broadcast.emit(data.userToNotify + '-' + data.mapid + '-UpdateMapperList', existingUser); @@ -25,7 +26,7 @@ function start() { // as a new mapper check whether there's a call in progress to join socket.on('checkForCall', function (data) { var socketsInRoom = io.sockets.clients(data.room); - if (socketsInRoom.length) socket.emit('maps-' + data.mapid + '-callInProgress', socketsInRoom.length); + if (socketsInRoom.length) socket.emit('maps-' + data.mapid + '-callInProgress'); }); // send the invitation to start a call socket.on('inviteACall', function (data) { @@ -43,6 +44,9 @@ function start() { socket.on('callDenied', function (data) { socket.broadcast.emit(data.inviter + '-' + data.mapid + '-callDenied', data.invited); }); + socket.on('inviteDenied', function (data) { + socket.broadcast.emit(data.inviter + '-' + data.mapid + '-inviteDenied', data.invited); + }); socket.on('mapperJoinedCall', function (data) { socket.broadcast.emit('maps-' + data.mapid + '-mapperJoinedCall', data.id); }); From dd457d8362c9dd558e776d9d6e8c226b97fe4ce5 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Tue, 1 Mar 2016 12:26:09 +1300 Subject: [PATCH 202/305] remove npm debug --- npm-debug.log | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 npm-debug.log diff --git a/npm-debug.log b/npm-debug.log deleted file mode 100644 index 021be352..00000000 --- a/npm-debug.log +++ /dev/null @@ -1,19 +0,0 @@ -0 info it worked if it ends with ok -1 verbose cli [ '/usr/bin/nodejs', '/usr/bin/npm', 'install' ] -2 info using npm@1.3.10 -3 info using node@v0.10.25 -4 error install Couldn't read dependencies -5 error Error: ENOENT, open '/vagrant/package.json' -6 error If you need help, you may report this log at: -6 error -6 error or email it to: -6 error -7 error System Linux 3.13.0-66-generic -8 error command "/usr/bin/nodejs" "/usr/bin/npm" "install" -9 error cwd /vagrant -10 error node -v v0.10.25 -11 error npm -v 1.3.10 -12 error path /vagrant/package.json -13 error code ENOENT -14 error errno 34 -15 verbose exit [ 34, true ] From 26025d6ee93db40bd3558faa11bcbeac0acea114 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Tue, 1 Mar 2016 12:30:49 +1300 Subject: [PATCH 203/305] store the right number of mappers in conversation --- app/assets/javascripts/src/Metamaps.js.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index 08eb6cfe..d9e1f04a 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -2534,6 +2534,7 @@ Metamaps.Realtime = { image: data.userimage, color: Metamaps.Util.getPastelColor(), realtime: data.userrealtime, + inConversation: data.userinconversation, coords: { x: 0, y: 0 From 616dc9ed0eded0d247874de1fe29a8d56b8b0f85 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Tue, 1 Mar 2016 12:55:00 +1300 Subject: [PATCH 204/305] messages weren't being displayed in order --- app/controllers/maps_controller.rb | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 3bc351d8..f2e3d239 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -34,7 +34,7 @@ class MapsController < ApplicationController end respond_to do |format| - format.html { + format.html { if @request == "active" && authenticated? redirect_to root_url and return end @@ -55,17 +55,17 @@ class MapsController < ApplicationController end respond_to do |format| - format.html { + format.html { @allmappers = @map.contributors @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } @allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } - @allmappings = @map.mappings.to_a.delete_if {|m| + @allmappings = @map.mappings.to_a.delete_if {|m| object = m.mappable !object || (object.permission == "private" && (!authenticated? || (authenticated? && @current.id != object.user_id))) } - @allmessages = @map.messages + @allmessages = @map.messages.sort_by(&:created_at) - respond_with(@allmappers, @allmappings, @allsynapses, @alltopics, @allmessages, @map) + respond_with(@allmappers, @allmappings, @allsynapses, @alltopics, @allmessages, @map) } format.json { render json: @map } format.csv { send_data @map.to_csv } @@ -86,7 +86,7 @@ class MapsController < ApplicationController @allmappers = @map.contributors @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && @current.id != t.user_id)) } @allsynapses = @map.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && @current.id != s.user_id)) } - @allmappings = @map.mappings.to_a.delete_if {|m| + @allmappings = @map.mappings.to_a.delete_if {|m| object = m.mappable !object || (object.permission == "private" && (!authenticated? || (authenticated? && @current.id != object.user_id))) } @@ -97,7 +97,7 @@ class MapsController < ApplicationController @json['synapses'] = @allsynapses @json['mappings'] = @allmappings @json['mappers'] = @allmappers - @json['messages'] = @map.messages + @json['messages'] = @map.messages.sort_by(&:created_at) respond_to do |format| format.json { render json: @json } @@ -112,7 +112,7 @@ class MapsController < ApplicationController @map.desc = params[:desc] @map.permission = params[:permission] @map.user = @user - @map.arranged = false + @map.arranged = false if params[:topicsToMap] @all = params[:topicsToMap] @@ -161,7 +161,7 @@ class MapsController < ApplicationController @map = Map.find(params[:id]).authorize_to_edit(@current) respond_to do |format| - if !@map + if !@map format.json { render json: "unauthorized" } elsif @map.update_attributes(map_params) format.json { head :no_content } @@ -184,7 +184,7 @@ class MapsController < ApplicationController data.content_type = "image/png" @map.screenshot = data end - + if @map.save render :json => {:message => "Successfully uploaded the map screenshot."} else @@ -204,7 +204,7 @@ class MapsController < ApplicationController @map.delete if @map respond_to do |format| - format.json { + format.json { if @map render json: "success" else From ef60aefe882589213e6df2c5a208362a65c8860e Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 29 Feb 2016 17:05:11 +1300 Subject: [PATCH 205/305] fix js bug --- app/assets/javascripts/src/Metamaps.GlobalUI.js.erb | 1 + app/assets/javascripts/src/Metamaps.js.erb | 10 ++++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb index 27812c57..00460ad4 100644 --- a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb +++ b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb @@ -48,6 +48,7 @@ $(document).ready(function () { // this runs the init function within each sub-object on the Metamaps one if (Metamaps.hasOwnProperty(prop) && + Metamaps[prop] != null && Metamaps[prop].hasOwnProperty('init') && typeof (Metamaps[prop].init) == 'function' ) { diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index 5a6c5955..ff6d57e6 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -17,12 +17,10 @@ var labelType, useGradients, nativeTextSupport, animate; })(); // TODO eliminate these 4 top-level variables -Metamaps = { - panningInt: null, - tempNode: null, - tempInit: false, - tempNode2: null -} +Metamaps.panningInt = null; +Metamaps.tempNode = null; +Metamaps.tempInit = false; +Metamaps.tempNode2 = null; Metamaps.Settings = { embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages From 059591b78b419ebdfedc79dc721aced625717724 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Fri, 11 Mar 2016 17:16:04 +1100 Subject: [PATCH 206/305] first pass at important API endpoints, token auth --- Gemfile | 2 + Gemfile.lock | 126 +++++++++++---------- app/assets/javascripts/src/Metamaps.js.erb | 4 +- app/controllers/api/mappings_controller.rb | 11 ++ app/controllers/api/maps_controller.rb | 11 ++ app/controllers/api/restful_controller.rb | 29 +++++ app/controllers/api/synapses_controller.rb | 11 ++ app/controllers/api/tokens_controller.rb | 21 ++++ app/controllers/api/topics_controller.rb | 11 ++ app/models/logged_out_user.rb | 7 ++ app/models/mapping.rb | 10 +- app/models/permitted_params.rb | 33 ++++++ app/models/synapse.rb | 2 + app/models/token.rb | 11 ++ app/models/user.rb | 5 + app/serializers/map_serializer.rb | 20 ++++ app/serializers/mapping_serializer.rb | 17 +++ app/serializers/metacode_serializer.rb | 7 ++ app/serializers/synapse_serializer.rb | 19 ++++ app/serializers/token_serializer.rb | 14 +++ app/serializers/topic_serializer.rb | 18 +++ app/serializers/user_serializer.rb | 15 +++ config/routes.rb | 10 ++ db/migrate/20160310200131_create_tokens.rb | 11 ++ db/schema.rb | 13 ++- postatoken.txt | 7 ++ postdata.txt | 1 + postmapping.txt | 1 + postsynapse.txt | 1 + posttopic.txt | 1 + spec/models/token_spec.rb | 5 + 31 files changed, 392 insertions(+), 62 deletions(-) create mode 100644 app/controllers/api/mappings_controller.rb create mode 100644 app/controllers/api/maps_controller.rb create mode 100644 app/controllers/api/restful_controller.rb create mode 100644 app/controllers/api/synapses_controller.rb create mode 100644 app/controllers/api/tokens_controller.rb create mode 100644 app/controllers/api/topics_controller.rb create mode 100644 app/models/logged_out_user.rb create mode 100644 app/models/permitted_params.rb create mode 100644 app/models/token.rb create mode 100644 app/serializers/map_serializer.rb create mode 100644 app/serializers/mapping_serializer.rb create mode 100644 app/serializers/metacode_serializer.rb create mode 100644 app/serializers/synapse_serializer.rb create mode 100644 app/serializers/token_serializer.rb create mode 100644 app/serializers/topic_serializer.rb create mode 100644 app/serializers/user_serializer.rb create mode 100644 db/migrate/20160310200131_create_tokens.rb create mode 100644 postatoken.txt create mode 100644 postdata.txt create mode 100644 postmapping.txt create mode 100644 postsynapse.txt create mode 100644 posttopic.txt create mode 100644 spec/models/token_spec.rb diff --git a/Gemfile b/Gemfile index 8379a7db..6b48c790 100644 --- a/Gemfile +++ b/Gemfile @@ -15,6 +15,8 @@ gem 'best_in_place' #in-place editing gem 'kaminari' # pagination gem 'uservoice-ruby' gem 'dotenv' +gem 'snorlax', '~> 0.1.3' +gem 'active_model_serializers', '~> 0.8.1' gem 'paperclip' gem 'aws-sdk', '< 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index eb8edec5..08c76b66 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -20,6 +20,8 @@ GEM erubis (~> 2.7.0) rails-dom-testing (~> 1.0, >= 1.0.5) rails-html-sanitizer (~> 1.0, >= 1.0.2) + active_model_serializers (0.8.3) + activemodel (>= 3.0) activejob (4.2.4) activesupport (= 4.2.4) globalid (>= 0.3.0) @@ -43,8 +45,8 @@ GEM aws-sdk-v1 (1.66.0) json (~> 1.4) nokogiri (>= 1.4.4) - bcrypt (3.1.10) - best_in_place (3.0.3) + bcrypt (3.1.11) + best_in_place (3.1.0) actionpack (>= 3.2) railties (>= 3.2) better_errors (2.1.1) @@ -54,24 +56,23 @@ GEM binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) builder (3.2.2) - byebug (5.0.0) - columnize (= 0.9.0) - cancancan (1.13.1) + byebug (8.2.2) + cancancan (1.10.1) climate_control (0.0.3) activesupport (>= 3.0) - cocaine (0.5.7) + cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) - coderay (1.1.0) - coffee-rails (4.1.0) + coderay (1.1.1) + coffee-rails (4.1.1) coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.0) + railties (>= 4.0.0, < 5.1.x) coffee-script (2.4.1) coffee-script-source execjs - coffee-script-source (1.9.1.1) - columnize (0.9.0) + coffee-script-source (1.10.0) + concurrent-ruby (1.0.1) debug_inspector (0.0.2) - devise (3.5.2) + devise (3.5.6) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 3.2.6, < 5) @@ -80,13 +81,13 @@ GEM warden (~> 1.2.3) diff-lcs (1.2.5) docile (1.1.5) - dotenv (2.0.2) + dotenv (2.1.0) erubis (2.7.0) execjs (2.6.0) ezcrypto (0.7.2) factory_girl (4.5.0) activesupport (>= 3.0.0) - factory_girl_rails (4.5.0) + factory_girl_rails (4.6.0) factory_girl (~> 4.5.0) railties (>= 3.0.0) formtastic (3.1.3) @@ -96,17 +97,17 @@ GEM globalid (0.3.6) activesupport (>= 4.1.0) i18n (0.7.0) - jbuilder (2.3.2) - activesupport (>= 3.0.0, < 5) + jbuilder (2.4.1) + activesupport (>= 3.0.0, < 5.1) multi_json (~> 1.2) - jquery-rails (4.0.5) - rails-dom-testing (~> 1.0) + jquery-rails (4.1.1) + rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) jquery-ui-rails (5.0.5) railties (>= 3.2.16) json (1.8.3) - json-schema (2.6.0) + json-schema (2.6.1) addressable (~> 2.3.8) kaminari (0.16.3) actionpack (>= 3.0.0) @@ -116,28 +117,28 @@ GEM mail (2.6.3) mime-types (>= 1.16, < 3) method_source (0.8.2) - mime-types (2.6.2) + mime-types (2.99.1) mimemagic (0.3.0) - mini_portile (0.6.2) - minitest (5.8.2) + mini_portile2 (2.0.0) + minitest (5.8.4) multi_json (1.11.2) - nokogiri (1.6.6.2) - mini_portile (~> 0.6.0) - oauth (0.4.7) + nokogiri (1.6.7.2) + mini_portile2 (~> 2.0.0.rc2) + oauth (0.5.1) orm_adapter (0.5.0) - paperclip (4.3.1) + paperclip (4.3.5) activemodel (>= 3.2.0) activesupport (>= 3.2.0) cocaine (~> 0.5.5) mime-types mimemagic (= 0.3.0) - pg (0.18.3) + pg (0.18.4) pry (0.10.3) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) - pry-byebug (3.2.0) - byebug (~> 5.0) + pry-byebug (3.3.0) + byebug (~> 8.0) pry (~> 0.10) pry-rails (0.3.4) pry (>= 0.9.10) @@ -163,66 +164,70 @@ GEM activesupport (>= 4.2.0.beta, < 5.0) nokogiri (~> 1.6.0) rails-deprecated_sanitizer (>= 1.0.1) - rails-html-sanitizer (1.0.2) + rails-html-sanitizer (1.0.3) loofah (~> 2.0) rails3-jquery-autocomplete (1.0.15) rails (>= 3.2) rails_12factor (0.0.3) rails_serve_static_assets rails_stdout_logging - rails_serve_static_assets (0.0.4) + rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.4) railties (4.2.4) actionpack (= 4.2.4) activesupport (= 4.2.4) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.4.2) - redis (3.2.1) - responders (2.1.0) - railties (>= 4.2.0, < 5) - rspec-core (3.3.2) - rspec-support (~> 3.3.0) - rspec-expectations (3.3.1) + rake (11.0.1) + redis (3.2.2) + responders (2.1.1) + railties (>= 4.2.0, < 5.1) + rspec-core (3.4.4) + rspec-support (~> 3.4.0) + rspec-expectations (3.4.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-mocks (3.3.2) + rspec-support (~> 3.4.0) + rspec-mocks (3.4.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.3.0) - rspec-rails (3.3.3) + rspec-support (~> 3.4.0) + rspec-rails (3.4.2) actionpack (>= 3.0, < 4.3) activesupport (>= 3.0, < 4.3) railties (>= 3.0, < 4.3) - rspec-core (~> 3.3.0) - rspec-expectations (~> 3.3.0) - rspec-mocks (~> 3.3.0) - rspec-support (~> 3.3.0) - rspec-support (3.3.0) - sass (3.4.19) + rspec-core (~> 3.4.0) + rspec-expectations (~> 3.4.0) + rspec-mocks (~> 3.4.0) + rspec-support (~> 3.4.0) + rspec-support (3.4.1) + sass (3.4.21) sass-rails (5.0.4) railties (>= 4.0.0, < 5.0) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - shoulda-matchers (3.0.1) + shoulda-matchers (3.1.1) activesupport (>= 4.0.0) - simplecov (0.11.1) + simplecov (0.11.2) docile (~> 1.1.0) json (~> 1.8) simplecov-html (~> 0.10.0) simplecov-html (0.10.0) slop (3.6.0) - sprockets (3.4.0) + snorlax (0.1.4) + cancancan (~> 1.10.1) + rails (> 4.1) + sprockets (3.5.2) + concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (2.3.3) - actionpack (>= 3.0) - activesupport (>= 3.0) - sprockets (>= 2.8, < 4.0) + sprockets-rails (3.0.4) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) thor (0.19.1) thread_safe (0.3.5) - tilt (2.0.1) - tunemygc (1.0.61) + tilt (2.0.2) + tunemygc (1.0.65) tzinfo (1.2.2) thread_safe (~> 0.1) uglifier (2.7.2) @@ -232,13 +237,14 @@ GEM ezcrypto (>= 0.7.2) json (>= 1.7.5) oauth (>= 0.4.7) - warden (1.2.3) + warden (1.2.6) rack (>= 1.0) PLATFORMS ruby DEPENDENCIES + active_model_serializers (~> 0.8.1) aws-sdk (< 2.0) best_in_place better_errors @@ -269,6 +275,10 @@ DEPENDENCIES sass-rails shoulda-matchers simplecov + snorlax (~> 0.1.3) tunemygc uglifier uservoice-ruby + +BUNDLED WITH + 1.11.2 diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index ff6d57e6..95cf7379 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -4083,7 +4083,7 @@ Metamaps.Topic = { }; var topicSuccessCallback = function (topicModel, response) { if (Metamaps.Active.Map) { - mapping.save({ mappable_id: topicModel.id }, { + mapping.save({ mappable_id: topicModel.get('topic').id }, { success: mappingSuccessCallback, error: function (model, response) { console.log('error saving mapping to database'); @@ -4254,7 +4254,7 @@ Metamaps.Synapse = { }; var synapseSuccessCallback = function (synapseModel, response) { if (Metamaps.Active.Map) { - mapping.save({ mappable_id: synapseModel.id }, { + mapping.save({ mappable_id: synapseModel.get('synapse').id }, { success: mappingSuccessCallback }); } diff --git a/app/controllers/api/mappings_controller.rb b/app/controllers/api/mappings_controller.rb new file mode 100644 index 00000000..83892b9a --- /dev/null +++ b/app/controllers/api/mappings_controller.rb @@ -0,0 +1,11 @@ +class Api::MappingsController < API::RestfulController + + def create + raise CanCan::AccessDenied.new unless current_user.is_logged_in? + instantiate_resouce + resource.user = current_user + create_action + respond_with_resource + end + +end diff --git a/app/controllers/api/maps_controller.rb b/app/controllers/api/maps_controller.rb new file mode 100644 index 00000000..2f86d254 --- /dev/null +++ b/app/controllers/api/maps_controller.rb @@ -0,0 +1,11 @@ +class Api::MapsController < API::RestfulController + + def create + raise CanCan::AccessDenied.new unless current_user.is_logged_in? + instantiate_resouce + resource.user = current_user + create_action + respond_with_resource + end + +end diff --git a/app/controllers/api/restful_controller.rb b/app/controllers/api/restful_controller.rb new file mode 100644 index 00000000..8289583d --- /dev/null +++ b/app/controllers/api/restful_controller.rb @@ -0,0 +1,29 @@ +class API::RestfulController < ActionController::Base + snorlax_used_rest! + + def show + load_resource + raise AccessDenied.new unless resource.authorize_to_show(current_user) + respond_with_resource + end + + private + + def current_user + super || token_user || LoggedOutUser.new + end + + def token_user + authenticate_with_http_token do |token, options| + access_token = Token.find_by_token(token) + if access_token + @token_user ||= access_token.user + end + end + end + + def permitted_params + @permitted_params ||= PermittedParams.new(params) + end + +end diff --git a/app/controllers/api/synapses_controller.rb b/app/controllers/api/synapses_controller.rb new file mode 100644 index 00000000..de435df3 --- /dev/null +++ b/app/controllers/api/synapses_controller.rb @@ -0,0 +1,11 @@ +class Api::SynapsesController < API::RestfulController + + def create + raise CanCan::AccessDenied.new unless current_user.is_logged_in? + instantiate_resouce + resource.user = current_user + create_action + respond_with_resource + end + +end diff --git a/app/controllers/api/tokens_controller.rb b/app/controllers/api/tokens_controller.rb new file mode 100644 index 00000000..2b2ff8df --- /dev/null +++ b/app/controllers/api/tokens_controller.rb @@ -0,0 +1,21 @@ +class Api::TokensController < API::RestfulController + + def create + raise CanCan::AccessDenied.new unless current_user.is_logged_in? + instantiate_resouce + resource.user = current_user + create_action + respond_with_resource + end + + def my_tokens + raise CanCan::AccessDenied.new unless current_user.is_logged_in? + instantiate_collection page_collection: false, timeframe_collection: false + respond_with_collection + end + + def visible_records + current_user.tokens + end + +end diff --git a/app/controllers/api/topics_controller.rb b/app/controllers/api/topics_controller.rb new file mode 100644 index 00000000..ded6a5e6 --- /dev/null +++ b/app/controllers/api/topics_controller.rb @@ -0,0 +1,11 @@ +class Api::TopicsController < API::RestfulController + + def create + raise CanCan::AccessDenied.new unless current_user.is_logged_in? + instantiate_resouce + resource.user = current_user + create_action + respond_with_resource + end + +end diff --git a/app/models/logged_out_user.rb b/app/models/logged_out_user.rb new file mode 100644 index 00000000..14e0cfb9 --- /dev/null +++ b/app/models/logged_out_user.rb @@ -0,0 +1,7 @@ +class LoggedOutUser + + FALSE_METHODS = [:is_logged_in?] + + FALSE_METHODS.each { |method| define_method(method, -> { false }) } + +end diff --git a/app/models/mapping.rb b/app/models/mapping.rb index d034de1c..8f29ed6a 100644 --- a/app/models/mapping.rb +++ b/app/models/mapping.rb @@ -25,5 +25,13 @@ class Mapping < ActiveRecord::Base def as_json(options={}) super(:methods =>[:user_name, :user_image]) end - + + def authorize_to_show(user) + if ((self.map.permission == "private" && self.map.user != user) || + (self.mappable.permission == "private" && self.mappable.user != user)) + return false + end + return self + end + end diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb new file mode 100644 index 00000000..a49ed648 --- /dev/null +++ b/app/models/permitted_params.rb @@ -0,0 +1,33 @@ +class PermittedParams < Struct.new(:params) + + %w[map synapse topic mapping token].each do |kind| + define_method(kind) do + permitted_attributes = self.send("#{kind}_attributes") + params.require(kind).permit(*permitted_attributes) + end + alias_method :"api_#{kind}", kind.to_sym + end + + alias :read_attribute_for_serialization :send + + def token_attributes + [:description] + end + + def map_attributes + [:name, :desc, :permission, :arranged] + end + + def synapse_attributes + [:desc, :category, :weight, :permission, :node1_id, :node2_id] + end + + def topic_attributes + [:name, :desc, :link, :permission, :metacode_id] + end + + def mapping_attributes + [:xloc, :yloc, :map_id, :mappable_type, :mappable_id] + end + +end diff --git a/app/models/synapse.rb b/app/models/synapse.rb index ea5889cc..c766d95c 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -10,6 +10,8 @@ class Synapse < ActiveRecord::Base validates :desc, length: { minimum: 0, allow_nil: false } validates :permission, presence: true + validates :node1_id, presence: true + validates :node2_id, presence: true validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) } validates :category, inclusion: { in: ['from-to', 'both'], allow_nil: true } diff --git a/app/models/token.rb b/app/models/token.rb new file mode 100644 index 00000000..15bdca79 --- /dev/null +++ b/app/models/token.rb @@ -0,0 +1,11 @@ +class Token < ActiveRecord::Base + belongs_to :user + + before_create :generate_token + + private + def generate_token + self.token = SecureRandom.uuid.gsub(/\-/,'') + end + +end diff --git a/app/models/user.rb b/app/models/user.rb index bd3bf477..a8fb6e6b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -6,6 +6,7 @@ class User < ActiveRecord::Base has_many :synapses has_many :maps has_many :mappings + has_many :tokens after_create :generate_code @@ -40,6 +41,10 @@ class User < ActiveRecord::Base # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :image, :content_type => /\Aimage\/.*\Z/ + def is_logged_in? + true + end + # override default as_json def as_json(options={}) { :id => self.id, diff --git a/app/serializers/map_serializer.rb b/app/serializers/map_serializer.rb new file mode 100644 index 00000000..b9839782 --- /dev/null +++ b/app/serializers/map_serializer.rb @@ -0,0 +1,20 @@ +class MapSerializer < ActiveModel::Serializer + embed :ids, include: true + attributes :id, + :name, + :desc, + :permission, + :screenshot, + :created_at, + :updated_at + + has_many :topics + has_many :synapses + has_many :mappings + has_many :contributors, root: :users + + #def filter(keys) + # keys.delete(:outcome_author) unless object.outcome_author.present? + # keys + #end +end diff --git a/app/serializers/mapping_serializer.rb b/app/serializers/mapping_serializer.rb new file mode 100644 index 00000000..400c35ba --- /dev/null +++ b/app/serializers/mapping_serializer.rb @@ -0,0 +1,17 @@ +class MappingSerializer < ActiveModel::Serializer + embed :ids, include: true + attributes :id, + :xloc, + :yloc, + :created_at, + :updated_at + has_one :user + has_one :map + has_one :mappable, polymorphic: true + + def filter(keys) + keys.delete(:xloc) unless object.mappable_type == "Topic" + keys.delete(:yloc) unless object.mappable_type == "Topic" + keys + end +end diff --git a/app/serializers/metacode_serializer.rb b/app/serializers/metacode_serializer.rb new file mode 100644 index 00000000..3918d274 --- /dev/null +++ b/app/serializers/metacode_serializer.rb @@ -0,0 +1,7 @@ +class MetacodeSerializer < ActiveModel::Serializer + attributes :id, + :name, + :manual_icon, + :color, + :aws_icon +end diff --git a/app/serializers/synapse_serializer.rb b/app/serializers/synapse_serializer.rb new file mode 100644 index 00000000..77c3a159 --- /dev/null +++ b/app/serializers/synapse_serializer.rb @@ -0,0 +1,19 @@ +class SynapseSerializer < ActiveModel::Serializer + embed :ids, include: true + attributes :id, + :desc, + :category, + :weight, + :permission, + :created_at, + :updated_at + + has_one :topic1, root: :topics + has_one :topic2, root: :topics + has_one :user + + #def filter(keys) + # keys.delete(:outcome_author) unless object.outcome_author.present? + # keys + #end +end diff --git a/app/serializers/token_serializer.rb b/app/serializers/token_serializer.rb new file mode 100644 index 00000000..777a14c1 --- /dev/null +++ b/app/serializers/token_serializer.rb @@ -0,0 +1,14 @@ +class TokenSerializer < ActiveModel::Serializer + embed :ids, include: true + attributes :id, + :token, + :description, + :user_id, + :created_at, + :updated_at + + #def filter(keys) + # keys.delete(:outcome_author) unless object.outcome_author.present? + # keys + #end +end diff --git a/app/serializers/topic_serializer.rb b/app/serializers/topic_serializer.rb new file mode 100644 index 00000000..205b8178 --- /dev/null +++ b/app/serializers/topic_serializer.rb @@ -0,0 +1,18 @@ +class TopicSerializer < ActiveModel::Serializer + embed :ids, include: true + attributes :id, + :name, + :desc, + :link, + :permission, + :created_at, + :updated_at + + has_one :user + has_one :metacode + + #def filter(keys) + # keys.delete(:outcome_author) unless object.outcome_author.present? + # keys + #end +end diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb new file mode 100644 index 00000000..b9a3b416 --- /dev/null +++ b/app/serializers/user_serializer.rb @@ -0,0 +1,15 @@ +class UserSerializer < ActiveModel::Serializer + attributes :id, + :name, + :avatar, + :is_admin, + :generation + + def avatar + object.image.url(:sixtyfour) + end + + def is_admin + object.admin + end +end diff --git a/config/routes.rb b/config/routes.rb index a3ab6e3a..1b0e5295 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,6 +9,16 @@ Metamaps::Application.routes.draw do get 'search/mappers', to: 'main#searchmappers', as: :searchmappers get 'search/synapses', to: 'main#searchsynapses', as: :searchsynapses + namespace :api, path: '/api/v1', defaults: {format: :json} do + resources :maps, only: [:create, :show, :update, :destroy] + resources :synapses, only: [:create, :show, :update, :destroy] + resources :topics, only: [:create, :show, :update, :destroy] + resources :mappings, only: [:create, :show, :update, :destroy] + resources :tokens, only: [ :create, :destroy] do + get :my_tokens, on: :collection + end + end + resources :mappings, except: [:index, :new, :edit] resources :metacode_sets, :except => [:show] resources :metacodes, :except => [:show, :destroy] diff --git a/db/migrate/20160310200131_create_tokens.rb b/db/migrate/20160310200131_create_tokens.rb new file mode 100644 index 00000000..7b9272a5 --- /dev/null +++ b/db/migrate/20160310200131_create_tokens.rb @@ -0,0 +1,11 @@ +class CreateTokens < ActiveRecord::Migration + def change + create_table :tokens do |t| + t.string :token + t.string :description + t.references :user, index: true, foreign_key: true + + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index e6a9bd11..334fd4ad 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160223061711) do +ActiveRecord::Schema.define(version: 20160310200131) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -117,6 +117,16 @@ ActiveRecord::Schema.define(version: 20160223061711) do add_index "synapses", ["node2_id"], name: "index_synapses_on_node2_id", using: :btree add_index "synapses", ["user_id"], name: "index_synapses_on_user_id", using: :btree + create_table "tokens", force: :cascade do |t| + t.string "token" + t.string "description" + t.integer "user_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "tokens", ["user_id"], name: "index_tokens_on_user_id", using: :btree + create_table "topics", force: :cascade do |t| t.text "name" t.text "desc" @@ -171,4 +181,5 @@ ActiveRecord::Schema.define(version: 20160223061711) do add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree + add_foreign_key "tokens", "users" end diff --git a/postatoken.txt b/postatoken.txt new file mode 100644 index 00000000..c9d98ff3 --- /dev/null +++ b/postatoken.txt @@ -0,0 +1,7 @@ +$.post('http://localhost:3000/api/v1/tokens', {token: { + description: 'for stuff', + token: '1234', + user_id: 2 +}}) + +curl -X POST -d @postdata.txt http://localhost:3000/api/v1/maps --header "Authorization: Token token=fb5b3db125c94e9fb50f1e42054be856" --header "Content-Type:application/json" diff --git a/postdata.txt b/postdata.txt new file mode 100644 index 00000000..9daaf598 --- /dev/null +++ b/postdata.txt @@ -0,0 +1 @@ +{ "map": { "name":"tree", "desc": "green", "permission": "commons", "arranged": true }} diff --git a/postmapping.txt b/postmapping.txt new file mode 100644 index 00000000..c9463f92 --- /dev/null +++ b/postmapping.txt @@ -0,0 +1 @@ +{ "mapping": { "xloc": 123, "yloc": 123, "map_id": 1, "mappable_type": "Topic", "mappable_id": 2 }} diff --git a/postsynapse.txt b/postsynapse.txt new file mode 100644 index 00000000..da2617d8 --- /dev/null +++ b/postsynapse.txt @@ -0,0 +1 @@ +{ "synapse": { "desc": "link between", "permission": "commons", "node1_id": 1, "node2_id": 2, "category": "from-to" }} diff --git a/posttopic.txt b/posttopic.txt new file mode 100644 index 00000000..e00d797f --- /dev/null +++ b/posttopic.txt @@ -0,0 +1 @@ +{ "topic": { "name":"tree topic", "desc": "so green", "permission": "commons", "metacode_id": 1 }} diff --git a/spec/models/token_spec.rb b/spec/models/token_spec.rb new file mode 100644 index 00000000..18bba17d --- /dev/null +++ b/spec/models/token_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Token, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From ccfba03fdb1ed5a858f3573f84026ee90c0b7c77 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Fri, 11 Mar 2016 17:26:54 +1100 Subject: [PATCH 207/305] clean up duplicate code --- app/controllers/api/mappings_controller.rb | 8 -------- app/controllers/api/maps_controller.rb | 8 -------- app/controllers/api/restful_controller.rb | 8 ++++++++ app/controllers/api/synapses_controller.rb | 8 -------- app/controllers/api/tokens_controller.rb | 10 +--------- app/controllers/api/topics_controller.rb | 8 -------- 6 files changed, 9 insertions(+), 41 deletions(-) diff --git a/app/controllers/api/mappings_controller.rb b/app/controllers/api/mappings_controller.rb index 83892b9a..426c9dbe 100644 --- a/app/controllers/api/mappings_controller.rb +++ b/app/controllers/api/mappings_controller.rb @@ -1,11 +1,3 @@ class Api::MappingsController < API::RestfulController - def create - raise CanCan::AccessDenied.new unless current_user.is_logged_in? - instantiate_resouce - resource.user = current_user - create_action - respond_with_resource - end - end diff --git a/app/controllers/api/maps_controller.rb b/app/controllers/api/maps_controller.rb index 2f86d254..7b805280 100644 --- a/app/controllers/api/maps_controller.rb +++ b/app/controllers/api/maps_controller.rb @@ -1,11 +1,3 @@ class Api::MapsController < API::RestfulController - def create - raise CanCan::AccessDenied.new unless current_user.is_logged_in? - instantiate_resouce - resource.user = current_user - create_action - respond_with_resource - end - end diff --git a/app/controllers/api/restful_controller.rb b/app/controllers/api/restful_controller.rb index 8289583d..d56783a0 100644 --- a/app/controllers/api/restful_controller.rb +++ b/app/controllers/api/restful_controller.rb @@ -1,6 +1,14 @@ class API::RestfulController < ActionController::Base snorlax_used_rest! + def create + raise CanCan::AccessDenied.new unless current_user.is_logged_in? + instantiate_resouce + resource.user = current_user + create_action + respond_with_resource + end + def show load_resource raise AccessDenied.new unless resource.authorize_to_show(current_user) diff --git a/app/controllers/api/synapses_controller.rb b/app/controllers/api/synapses_controller.rb index de435df3..f133ffd0 100644 --- a/app/controllers/api/synapses_controller.rb +++ b/app/controllers/api/synapses_controller.rb @@ -1,11 +1,3 @@ class Api::SynapsesController < API::RestfulController - def create - raise CanCan::AccessDenied.new unless current_user.is_logged_in? - instantiate_resouce - resource.user = current_user - create_action - respond_with_resource - end - end diff --git a/app/controllers/api/tokens_controller.rb b/app/controllers/api/tokens_controller.rb index 2b2ff8df..cc54e531 100644 --- a/app/controllers/api/tokens_controller.rb +++ b/app/controllers/api/tokens_controller.rb @@ -1,13 +1,5 @@ class Api::TokensController < API::RestfulController - - def create - raise CanCan::AccessDenied.new unless current_user.is_logged_in? - instantiate_resouce - resource.user = current_user - create_action - respond_with_resource - end - + def my_tokens raise CanCan::AccessDenied.new unless current_user.is_logged_in? instantiate_collection page_collection: false, timeframe_collection: false diff --git a/app/controllers/api/topics_controller.rb b/app/controllers/api/topics_controller.rb index ded6a5e6..f3633544 100644 --- a/app/controllers/api/topics_controller.rb +++ b/app/controllers/api/topics_controller.rb @@ -1,11 +1,3 @@ class Api::TopicsController < API::RestfulController - def create - raise CanCan::AccessDenied.new unless current_user.is_logged_in? - instantiate_resouce - resource.user = current_user - create_action - respond_with_resource - end - end From d8cc588efb9ea11a43a3a9b6da71471068aff37f Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 11 Mar 2016 21:25:24 +0800 Subject: [PATCH 208/305] basics of admin_override policy function --- app/policies/application_policy.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index 2a0bbc52..6bd56c64 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -34,6 +34,14 @@ class ApplicationPolicy false end + # TODO update this function to enable some flag in the interface + # so that admins usually can't do super admin stuff unless they + # explicitly say they want to (E.g. seeing/editing/deleting private + # maps - they should be able to, but not by accident) + def admin_override + user.admin + end + def scope Pundit.policy_scope!(user, record.class) end From 615eaf580eb68a15e195338bf62d11e25f45907b Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 11 Mar 2016 21:30:54 +0800 Subject: [PATCH 209/305] mapping policy --- app/policies/mapping_policy.rb | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 app/policies/mapping_policy.rb diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb new file mode 100644 index 00000000..44e7bfd7 --- /dev/null +++ b/app/policies/mapping_policy.rb @@ -0,0 +1,31 @@ +class MappingPolicy < ApplicationPolicy + class Scope < Scope + def resolve + # TODO base this on the map policy + # it would be nice if we could also base this on the mappable, but that + # gets really complicated. Devin thinks it's OK to SHOW a mapping for + # a private topic, since you can't see the private topic anyways + scope.joins(:maps).where('maps.permission IN ("public", "commons") OR user_id = ?', user.id) + end + end + + def show? + map = policy(record.map, user) + mappable = policy(record.mappable, user) + map.show? && mappable.show? + end + + def create? + map = policy(record.map, user) + map.edit? + end + + def update? + map = policy(record.map, user) + map.update? + end + + def destroy? + record.user == user || admin_override + end +end From 73b82801cc8d5712d3b100ac56b195cacd5bd564 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 11 Mar 2016 21:32:18 +0800 Subject: [PATCH 210/305] consistent permissions --- app/policies/map_policy.rb | 2 +- app/policies/synapse_policy.rb | 8 +++----- app/policies/topic_policy.rb | 5 ++--- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 2cdbdee2..671eea83 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -43,6 +43,6 @@ class MapPolicy < ApplicationPolicy end def destroy? - record.user == user || user.admin + record.user == user || admin_override end end diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 6d332fbf..6763014a 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -10,16 +10,14 @@ class SynapsePolicy < ApplicationPolicy end def show? - # record.permission == 'commons' || record.permission == 'public' || record.user == user - true + record.permission == 'commons' || record.permission == 'public' || record.user == user end def update? - # user.present? && (record.permission == 'commons' || record.user == user) - true + user.present? && (record.permission == 'commons' || record.user == user) end def destroy? - record.user == user || user.admin + record.user == user || admin_override end end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 55e79c2d..03b42895 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -14,12 +14,11 @@ class TopicPolicy < ApplicationPolicy end def update? - # user.present? && (record.permission == 'commons' || record.user == user) - true + user.present? && (record.permission == 'commons' || record.user == user) end def destroy? - record.user == user || user.admin + record.user == user || admin_override end def autocomplete_topic? From 7395811ba5cdb23442710d537082d6821558850f Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 11 Mar 2016 21:35:48 +0800 Subject: [PATCH 211/305] handle unauthorized with baaaaad 403 --- app/controllers/application_controller.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0e6503ef..6d10c553 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,6 @@ class ApplicationController < ActionController::Base include Pundit + rescue_from Pundit::NotAuthorizedError, with: :handle_unauthorized protect_from_forgery before_action :get_invite_link @@ -23,6 +24,10 @@ class ApplicationController < ActionController::Base stored_location_for(resource) || request.referer || root_path end end + + def handle_unauthorized + head :forbidden # TODO make this better + end private From eb5675506811266ead0e4a95a914d31ec0fc0011 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 11 Mar 2016 22:10:31 +0800 Subject: [PATCH 212/305] implement five policies into their controllers --- app/controllers/main_controller.rb | 3 + app/controllers/mappings_controller.rb | 8 +- app/controllers/maps_controller.rb | 80 +++++----- app/controllers/synapses_controller.rb | 16 +- app/controllers/topics_controller.rb | 209 ++++++++++--------------- 5 files changed, 142 insertions(+), 174 deletions(-) diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 31a55abc..29c11777 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -3,6 +3,9 @@ class MainController < ApplicationController include MapsHelper include UsersHelper include SynapsesHelper + + after_action :verify_authorized, except: :index + after_action :verify_policy_scoped, only: :index respond_to :html, :json diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index 6ce37234..ea2aaf0e 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -1,12 +1,14 @@ class MappingsController < ApplicationController - before_action :require_user, only: [:create, :update, :destroy] + after_action :verify_authorized, except: :index + after_action :verify_policy_scoped, only: :index respond_to :json # GET /mappings/1.json def show @mapping = Mapping.find(params[:id]) + authorize! @mapping render json: @mapping end @@ -14,6 +16,7 @@ class MappingsController < ApplicationController # POST /mappings.json def create @mapping = Mapping.new(mapping_params) + authorize! @mapping if @mapping.save render json: @mapping, status: :created @@ -25,6 +28,7 @@ class MappingsController < ApplicationController # PUT /mappings/1.json def update @mapping = Mapping.find(params[:id]) + authorize! @mapping if @mapping.update_attributes(mapping_params) head :no_content @@ -36,7 +40,7 @@ class MappingsController < ApplicationController # DELETE /mappings/1.json def destroy @mapping = Mapping.find(params[:id]) - @map = @mapping.map + authorize! @mapping @mapping.destroy diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 9fbf95e0..016ba7b5 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,5 +1,7 @@ class MapsController < ApplicationController before_action :require_user, only: [:create, :update, :screenshot, :destroy] + after_action :verify_authorized, except: :activemaps, :featuredmaps, :mymaps, :usermaps + after_action :verify_policy_scoped, only: :activemaps, :featuredmaps, :mymaps, :usermaps respond_to :html, :json @@ -8,7 +10,8 @@ class MapsController < ApplicationController # GET /explore/active def activemaps page = params[:page].present? ? params[:page] : 1 - @maps = Map.where("maps.permission != ?", "private").order("updated_at DESC").page(page).per(20) + @maps = policy_scope(Map).order("updated_at DESC") + .page(page).per(20) # root url => main/home. main/home renders maps/activemaps view. redirect_to root_url and return if authenticated? @@ -22,8 +25,10 @@ class MapsController < ApplicationController # GET /explore/featured def featuredmaps page = params[:page].present? ? params[:page] : 1 - @maps = Map.where("maps.featured = ? AND maps.permission != ?", true, "private") - .order("updated_at DESC").page(page).per(20) + @maps = policy_scope( + Map.where("maps.featured = ? AND maps.permission != ?", + true, "private") + ).order("updated_at DESC").page(page).per(20) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -36,8 +41,9 @@ class MapsController < ApplicationController return redirect_to activemaps_url if !authenticated? page = params[:page].present? ? params[:page] : 1 - # don't need to exclude private maps because they all belong to you - @maps = Map.where("maps.user_id = ?", current_user.id).order("updated_at DESC").page(page).per(20) + @maps = policy_scope( + Map.where("maps.user_id = ?", current_user.id) + ).order("updated_at DESC").page(page).per(20) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -49,7 +55,8 @@ class MapsController < ApplicationController def usermaps page = params[:page].present? ? params[:page] : 1 @user = User.find(params[:id]) - @maps = Map.where("maps.user_id = ? AND maps.permission != ?", @user.id, "private").order("updated_at DESC").page(page).per(20) + @maps = policy_scope(Map.where(user: @user)) + .order("updated_at DESC").page(page).per(20) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -59,7 +66,8 @@ class MapsController < ApplicationController # GET maps/:id def show - @map = Map.find(params[:id]).authorize_to_show(current_user) + @map = Map.find(params[:id]) + authorize! @map if not @map redirect_to root_url, notice: "Access denied. That map is private." and return @@ -83,7 +91,8 @@ class MapsController < ApplicationController # GET maps/:id/contains def contains - @map = Map.find(params[:id]).authorize_to_show(current_user) + @map = Map.find(params[:id]) + authorize! @map if not @map redirect_to root_url, notice: "Access denied. That map is private." and return @@ -130,6 +139,7 @@ class MapsController < ApplicationController mapping.xloc = topic[1] mapping.yloc = topic[2] @map.topicmappings << mapping + authorize! mapping, :create mapping.save end @@ -142,6 +152,7 @@ class MapsController < ApplicationController mapping.map = @map mapping.mappable = Synapse.find(synapse_id) @map.synapsemappings << mapping + authorize! mapping, :create mapping.save end end @@ -149,6 +160,8 @@ class MapsController < ApplicationController @map.arranged = true end + authorize! @map + if @map.save respond_to do |format| format.json { render :json => @map } @@ -162,7 +175,8 @@ class MapsController < ApplicationController # PUT maps/:id def update - @map = Map.find(params[:id]).authorize_to_edit(current_user) + @map = Map.find(params[:id]) + authorize! @map respond_to do |format| if !@map @@ -177,42 +191,36 @@ class MapsController < ApplicationController # POST maps/:id/upload_screenshot def screenshot - @map = Map.find(params[:id]).authorize_to_edit(current_user) + @map = Map.find(params[:id]) + authorize! @map - if @map - png = Base64.decode64(params[:encoded_image]['data:image/png;base64,'.length .. -1]) - StringIO.open(png) do |data| - data.class.class_eval { attr_accessor :original_filename, :content_type } - data.original_filename = "map-" + @map.id.to_s + "-screenshot.png" - data.content_type = "image/png" - @map.screenshot = data - end + png = Base64.decode64(params[:encoded_image]['data:image/png;base64,'.length .. -1]) + StringIO.open(png) do |data| + data.class.class_eval { attr_accessor :original_filename, :content_type } + data.original_filename = "map-" + @map.id.to_s + "-screenshot.png" + data.content_type = "image/png" + @map.screenshot = data + end - if @map.save - render :json => {:message => "Successfully uploaded the map screenshot."} - else - render :json => {:message => "Failed to upload image."} - end - else - render :json => {:message => "Unauthorized to set map screenshot."} - end + if @map.save + render :json => {:message => "Successfully uploaded the map screenshot."} + else + render :json => {:message => "Failed to upload image."} + end end # DELETE maps/:id def destroy - @map = Map.find(params[:id]).authorize_to_delete(current_user) + @map = Map.find(params[:id]) + authorize! @map - @map.delete if @map + @map.delete - respond_to do |format| - format.json { - if @map - render json: "success" - else - render json: "unauthorized" - end - } + respond_to do |format| + format.json do + head :no_content end + end end private diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index 46592dcc..f242ad38 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -2,19 +2,16 @@ class SynapsesController < ApplicationController include TopicsHelper before_action :require_user, only: [:create, :update, :destroy] + after_action :verify_authorized, except: :index + after_action :verify_policy_scoped, only: :index respond_to :json # GET /synapses/1.json def show @synapse = Synapse.find(params[:id]) + authorize! @synapse - #.authorize_to_show(current_user) - - #if not @synapse - # redirect_to root_url and return - #end - render json: @synapse end @@ -23,6 +20,7 @@ class SynapsesController < ApplicationController def create @synapse = Synapse.new(synapse_params) @synapse.desc = "" if @synapse.desc.nil? + authorize! @synapse respond_to do |format| if @synapse.save @@ -38,6 +36,7 @@ class SynapsesController < ApplicationController def update @synapse = Synapse.find(params[:id]) @synapse.desc = "" if @synapse.desc.nil? + authorize! @synapse respond_to do |format| if @synapse.update_attributes(synapse_params) @@ -50,8 +49,9 @@ class SynapsesController < ApplicationController # DELETE synapses/:id def destroy - @synapse = Synapse.find(params[:id]).authorize_to_delete(current_user) - @synapse.delete if @synapse + @synapse = Synapse.find(params[:id]) + authorize! @synapse + @synapse.delete respond_to do |format| format.json { head :no_content } diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 4d11f8ca..125005f9 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -2,19 +2,15 @@ class TopicsController < ApplicationController include TopicsHelper before_action :require_user, only: [:create, :update, :destroy] - + after_action :verify_authorized + respond_to :html, :js, :json # GET /topics/autocomplete_topic def autocomplete_topic term = params[:term] if term && !term.empty? - @topics = Topic.where('LOWER("name") like ?', term.downcase + '%').order('"name"') - - #read this next line as 'delete a topic if its private and you're either - #1. logged out or 2. logged in but not the topic creator - @topics.to_a.delete_if {|t| t.permission == "private" && - (!authenticated? || (authenticated? && current_user.id != t.user_id)) } + @topics = policy_scope(Topic.where('LOWER("name") like ?', term.downcase + '%')).order('"name"') else @topics = [] end @@ -23,28 +19,16 @@ class TopicsController < ApplicationController # GET topics/:id def show - @topic = Topic.find(params[:id]).authorize_to_show(current_user) - - if not @topic - redirect_to root_url, notice: "Access denied. That topic is private." and return - end + @topic = Topic.find(params[:id]) + authorize! @topic respond_to do |format| format.html { - @alltopics = ([@topic] + @topic.relatives).delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id)) } # should limit to topics visible to user - @allsynapses = @topic.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && current_user.id != s.user_id)) } + @alltopics = ([@topic] + policy_scope(@topic.relatives) + @allsynapses = policy_scope(@topic.synapses) - @allcreators = [] - @alltopics.each do |t| - if @allcreators.index(t.user) == nil - @allcreators.push(t.user) - end - end - @allsynapses.each do |s| - if @allcreators.index(s.user) == nil - @allcreators.push(s.user) - end - end + @allcreators = @alltopics.map(&:user).uniq + @allcreators += @allsynapses.map(&:user).uniq respond_with(@allsynapses, @alltopics, @allcreators, @topic) } @@ -54,27 +38,15 @@ class TopicsController < ApplicationController # GET topics/:id/network def network - @topic = Topic.find(params[:id]).authorize_to_show(current_user) + @topic = Topic.find(params[:id]) + authorize! @topic - if not @topic - redirect_to root_url, notice: "Access denied. That topic is private." and return - end + @alltopics = [@topic] + policy_scope(@topic.relatives) + @allsynapses = policy_scope(@topic.synapses) + + @allcreators = @alltopics.map(&:user).uniq + @allcreators += @allsynapses.map(&:user).uniq - @alltopics = @topic.relatives.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id)) } - @allsynapses = @topic.synapses.to_a.delete_if {|s| s.permission == "private" && (!authenticated? || (authenticated? && current_user.id != s.user_id)) } - @allcreators = [] - @allcreators.push(@topic.user) - @alltopics.each do |t| - if @allcreators.index(t.user) == nil - @allcreators.push(t.user) - end - end - @allsynapses.each do |s| - if @allcreators.index(s.user) == nil - @allcreators.push(s.user) - end - end - @json = Hash.new() @json['topic'] = @topic @json['creators'] = @allcreators @@ -88,118 +60,99 @@ class TopicsController < ApplicationController # GET topics/:id/relative_numbers def relative_numbers - @topic = Topic.find(params[:id]).authorize_to_show(current_user) + @topic = Topic.find(params[:id]) + authorize @topic - if not @topic - redirect_to root_url, notice: "Access denied. That topic is private." and return + topicsAlreadyHas = params[:network] ? params[:network].split(',').map(&:to_i) : [] + + @alltopics = policy_scope(@topic.relatives).to_a.uniq + @alltopics.delete_if! do |topic| + topicsAlreadyHas.index(topic.id) != nil end - @topicsAlreadyHas = params[:network] ? params[:network].split(',') : [] - - @alltopics = @topic.relatives.to_a.delete_if {|t| - @topicsAlreadyHas.index(t.id.to_s) != nil || - (t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id))) - } - - @alltopics.uniq! - - @json = Hash.new() + @json = Hash.new(0) @alltopics.each do |t| - if @json[t.metacode.id] - @json[t.metacode.id] += 1 - else - @json[t.metacode.id] = 1 - end + @json[t.metacode.id] += 1 end respond_to do |format| - format.json { render json: @json } + format.json { render json: @json } end end # GET topics/:id/relatives def relatives - @topic = Topic.find(params[:id]).authorize_to_show(current_user) + @topic = Topic.find(params[:id]) + authorize! @topic - if not @topic - redirect_to root_url, notice: "Access denied. That topic is private." and return - end + topicsAlreadyHas = params[:network] ? params[:network].split(',').map(&:to_i) : [] - @topicsAlreadyHas = params[:network] ? params[:network].split(',') : [] + alltopics = policy_scope(@topic.relatives).to_a.uniq.delete_if do |topic| + topicsAlreadyHas.index(topic.id.to_s) != nil + end - @alltopics = @topic.relatives.to_a.delete_if {|t| - @topicsAlreadyHas.index(t.id.to_s) != nil || - (params[:metacode] && t.metacode_id.to_s != params[:metacode]) || - (t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id))) - } + #find synapses between topics in alltopics array + allsynapses = policy_scope(@topic.synapses) + synapse_ids = (allsynapses.map(&:topic1_id) + allsynapses.map(&:topic2_id)).uniq + allsynapses.delete_if! do |synapse| + synapse_ids.index(synapse.id) != nil + end - @alltopics.uniq! + creatorsAlreadyHas = params[:creators] ? params[:creators].split(',').map(&:to_i) : [] + allcreators = (alltopics.map(&:user) + allsynapses.map(&:user)).uniq.delete_if do |user| + creatorsAlreadyHas.index(user.id) != nil + end - @allsynapses = @topic.synapses.to_a.delete_if {|s| - (s.topic1 == @topic && @alltopics.index(s.topic2) == nil) || - (s.topic2 == @topic && @alltopics.index(s.topic1) == nil) - } + @json = Hash.new() + @json['topics'] = alltopics + @json['synapses'] = allsynapses + @json['creators'] = allcreators - @creatorsAlreadyHas = params[:creators] ? params[:creators].split(',') : [] - @allcreators = [] - @alltopics.each do |t| - if @allcreators.index(t.user) == nil && @creatorsAlreadyHas.index(t.user_id.to_s) == nil - @allcreators.push(t.user) - end - end - @allsynapses.each do |s| - if @allcreators.index(s.user) == nil && @creatorsAlreadyHas.index(s.user_id.to_s) == nil - @allcreators.push(s.user) - end - end - - @json = Hash.new() - @json['topics'] = @alltopics - @json['synapses'] = @allsynapses - @json['creators'] = @allcreators - - respond_to do |format| - format.json { render json: @json } - end + respond_to do |format| + format.json { render json: @json } + end end - # POST /topics - # POST /topics.json - def create - @topic = Topic.new(topic_params) + # POST /topics + # POST /topics.json + def create + @topic = Topic.new(topic_params) + authorize! @topic - respond_to do |format| - if @topic.save - format.json { render json: @topic, status: :created } - else - format.json { render json: @topic.errors, status: :unprocessable_entity } - end - end + respond_to do |format| + if @topic.save + format.json { render json: @topic, status: :created } + else + format.json { render json: @topic.errors, status: :unprocessable_entity } + end end + end - # PUT /topics/1 - # PUT /topics/1.json - def update - @topic = Topic.find(params[:id]) + # PUT /topics/1 + # PUT /topics/1.json + def update + @topic = Topic.find(params[:id]) + authorize! @topic - respond_to do |format| - if @topic.update_attributes(topic_params) - format.json { head :no_content } - else - format.json { render json: @topic.errors, status: :unprocessable_entity } - end - end + respond_to do |format| + if @topic.update_attributes(topic_params) + format.json { head :no_content } + else + format.json { render json: @topic.errors, status: :unprocessable_entity } + end end + end - # DELETE topics/:id - def destroy - @topic = Topic.find(params[:id]).authorize_to_delete(current_user) - @topic.delete if @topic + # DELETE topics/:id + def destroy + @topic = Topic.find(params[:id]) + authorize! @topic - respond_to do |format| - format.json { head :no_content } - end + @topic.delete + respond_to do |format| + format.json { head :no_content } end + end private From 669b337d04815d136d5b97ee3af638509fcc7a46 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 09:37:18 +1100 Subject: [PATCH 213/305] changes for api --- Gemfile | 2 ++ Gemfile.lock | 10 +++++++--- app/controllers/api/restful_controller.rb | 24 +++++++++++++++-------- app/controllers/api/tokens_controller.rb | 4 +++- app/models/logged_out_user.rb | 7 ------- 5 files changed, 28 insertions(+), 19 deletions(-) delete mode 100644 app/models/logged_out_user.rb diff --git a/Gemfile b/Gemfile index a0600ced..b7ea08e9 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,8 @@ gem 'devise' gem 'redis' gem 'pg' gem 'pundit' +gem 'cancan' +gem 'pundit_extra' gem 'formula' gem 'formtastic' gem 'json' diff --git a/Gemfile.lock b/Gemfile.lock index 9e9731a6..a2a3b031 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -56,8 +56,9 @@ GEM binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) builder (3.2.2) - byebug (5.0.0) - columnize (= 0.9.0) + byebug (8.2.2) + cancan (1.6.10) + cancancan (1.10.1) climate_control (0.0.3) activesupport (>= 3.0) cocaine (0.5.8) @@ -144,6 +145,7 @@ GEM pry (>= 0.9.10) pundit (1.1.0) activesupport (>= 3.0.0) + pundit_extra (0.1.1) quiet_assets (1.1.0) railties (>= 3.1, < 5.0) rack (1.6.4) @@ -180,7 +182,7 @@ GEM activesupport (= 4.2.4) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (11.0.1) + rake (11.1.0) redis (3.2.2) responders (2.1.1) railties (>= 4.2.0, < 5.1) @@ -251,6 +253,7 @@ DEPENDENCIES best_in_place better_errors binding_of_caller + cancan coffee-rails devise dotenv @@ -268,6 +271,7 @@ DEPENDENCIES pry-byebug pry-rails pundit + pundit_extra quiet_assets rails (= 4.2.4) rails3-jquery-autocomplete diff --git a/app/controllers/api/restful_controller.rb b/app/controllers/api/restful_controller.rb index d56783a0..ca7ff481 100644 --- a/app/controllers/api/restful_controller.rb +++ b/app/controllers/api/restful_controller.rb @@ -1,24 +1,32 @@ class API::RestfulController < ActionController::Base + include Pundit + include PunditExtra + snorlax_used_rest! + rescue_from(Pundit::NotAuthorizedError) { |e| respond_with_standard_error e, 403 } + load_and_authorize_resource except: [:index, :create] + def create - raise CanCan::AccessDenied.new unless current_user.is_logged_in? + authorize resource_class instantiate_resouce resource.user = current_user create_action respond_with_resource end - def show - load_resource - raise AccessDenied.new unless resource.authorize_to_show(current_user) - respond_with_resource - end - private + def accessible_records + if current_user + visible_records + else + public_records + end + end + def current_user - super || token_user || LoggedOutUser.new + super || token_user || nil end def token_user diff --git a/app/controllers/api/tokens_controller.rb b/app/controllers/api/tokens_controller.rb index cc54e531..6ef01e69 100644 --- a/app/controllers/api/tokens_controller.rb +++ b/app/controllers/api/tokens_controller.rb @@ -1,7 +1,9 @@ class Api::TokensController < API::RestfulController + + skip_authorization def my_tokens - raise CanCan::AccessDenied.new unless current_user.is_logged_in? + raise Pundit::NotAuthorizedError.new unless current_user.is_logged_in? instantiate_collection page_collection: false, timeframe_collection: false respond_with_collection end diff --git a/app/models/logged_out_user.rb b/app/models/logged_out_user.rb deleted file mode 100644 index 14e0cfb9..00000000 --- a/app/models/logged_out_user.rb +++ /dev/null @@ -1,7 +0,0 @@ -class LoggedOutUser - - FALSE_METHODS = [:is_logged_in?] - - FALSE_METHODS.each { |method| define_method(method, -> { false }) } - -end From 450db5eb8d2e641eefcfa1b9642223108d12a943 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 09:37:32 +1100 Subject: [PATCH 214/305] changes for pundit --- app/controllers/application_controller.rb | 1 + app/controllers/main_controller.rb | 4 +-- app/controllers/maps_controller.rb | 36 ++++++++--------------- app/models/map.rb | 29 ------------------ app/models/synapse.rb | 26 ---------------- app/models/topic.rb | 27 ----------------- app/policies/application_policy.rb | 2 +- app/policies/map_policy.rb | 2 +- app/policies/mapping_policy.rb | 2 +- 9 files changed, 19 insertions(+), 110 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6d10c553..d030c6e6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,6 @@ class ApplicationController < ActionController::Base include Pundit + include PunditExtra rescue_from Pundit::NotAuthorizedError, with: :handle_unauthorized protect_from_forgery diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 29c11777..efebdce7 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -4,8 +4,8 @@ class MainController < ApplicationController include UsersHelper include SynapsesHelper - after_action :verify_authorized, except: :index - after_action :verify_policy_scoped, only: :index +# after_action :verify_authorized, except: :index +# after_action :verify_policy_scoped, only: :index respond_to :html, :json diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index a778de9a..fd169083 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,8 +1,8 @@ class MapsController < ApplicationController before_action :require_user, only: [:create, :update, :screenshot, :destroy] - after_action :verify_authorized, except: :activemaps, :featuredmaps, :mymaps, :usermaps - after_action :verify_policy_scoped, only: :activemaps, :featuredmaps, :mymaps, :usermaps + after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :usermaps] + after_action :verify_policy_scoped, only: [:activemaps, :featuredmaps, :mymaps, :usermaps] respond_to :html, :json @@ -68,11 +68,7 @@ class MapsController < ApplicationController # GET maps/:id def show @map = Map.find(params[:id]) - authorize! @map - - if not @map - redirect_to root_url, notice: "Access denied. That map is private." and return - end + authorize @map respond_to do |format| format.html { @@ -86,18 +82,14 @@ class MapsController < ApplicationController respond_with(@allmappers, @allmappings, @allsynapses, @alltopics, @map) } - format.json { render json: @map } + format.json { render json: @map.as_json } end end # GET maps/:id/contains def contains @map = Map.find(params[:id]) - authorize! @map - - if not @map - redirect_to root_url, notice: "Access denied. That map is private." and return - end + authorize @map @allmappers = @map.contributors @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id)) } @@ -140,7 +132,7 @@ class MapsController < ApplicationController mapping.xloc = topic[1] mapping.yloc = topic[2] @map.topicmappings << mapping - authorize! mapping, :create + authorize mapping, :create mapping.save end @@ -153,7 +145,7 @@ class MapsController < ApplicationController mapping.map = @map mapping.mappable = Synapse.find(synapse_id) @map.synapsemappings << mapping - authorize! mapping, :create + authorize mapping, :create mapping.save end end @@ -161,7 +153,7 @@ class MapsController < ApplicationController @map.arranged = true end - authorize! @map + authorize @map if @map.save respond_to do |format| @@ -177,12 +169,10 @@ class MapsController < ApplicationController # PUT maps/:id def update @map = Map.find(params[:id]) - authorize! @map + authorize @map respond_to do |format| - if !@map - format.json { render json: "unauthorized" } - elsif @map.update_attributes(map_params) + if @map.update_attributes(map_params) format.json { head :no_content } else format.json { render json: @map.errors, status: :unprocessable_entity } @@ -193,7 +183,7 @@ class MapsController < ApplicationController # POST maps/:id/upload_screenshot def screenshot @map = Map.find(params[:id]) - authorize! @map + authorize @map png = Base64.decode64(params[:encoded_image]['data:image/png;base64,'.length .. -1]) StringIO.open(png) do |data| @@ -213,7 +203,7 @@ class MapsController < ApplicationController # DELETE maps/:id def destroy @map = Map.find(params[:id]) - authorize! @map + authorize @map @map.delete @@ -228,6 +218,6 @@ class MapsController < ApplicationController # Never trust parameters from the scary internet, only allow the white list through. def map_params - params.require(:map).permit(:id, :name, :arranged, :desc, :permission, :user_id) + params.require(:map).permit(:id, :name, :arranged, :desc, :permission) end end diff --git a/app/models/map.rb b/app/models/map.rb index 6c2caca2..87c8d641 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -78,36 +78,7 @@ class Map < ActiveRecord::Base json[:updated_at_clean] = updated_at_str json end - - ##### PERMISSIONS ###### - def authorize_to_delete(user) - if (self.user != user) - return false - end - return self - end - - # returns false if user not allowed to 'show' Topic, Synapse, or Map - def authorize_to_show(user) - if (self.permission == "private" && self.user != user) - return false - end - return self - end - - # returns false if user not allowed to 'edit' Topic, Synapse, or Map - def authorize_to_edit(user) - if !user - return false - elsif (self.permission == "private" && self.user != user) - return false - elsif (self.permission == "public" && self.user != user) - return false - end - return self - end - def decode_base64(imgBase64) decoded_data = Base64.decode64(imgBase64) diff --git a/app/models/synapse.rb b/app/models/synapse.rb index c766d95c..d545a25c 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -34,30 +34,4 @@ class Synapse < ActiveRecord::Base end # :nocov: - ##### PERMISSIONS ###### - - # returns false if user not allowed to 'show' Topic, Synapse, or Map - def authorize_to_show(user) - if (self.permission == "private" && self.user != user) - return false - end - return self - end - - # returns false if user not allowed to 'edit' Topic, Synapse, or Map - def authorize_to_edit(user) - if (self.permission == "private" && self.user != user) - return false - elsif (self.permission == "public" && self.user != user) - return false - end - return self - end - - def authorize_to_delete(user) - if (self.user == user || user.admin) - return self - end - return false - end end diff --git a/app/models/topic.rb b/app/models/topic.rb index c528aa6e..0039040e 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -87,31 +87,4 @@ class Topic < ActiveRecord::Base end result end - - ##### PERMISSIONS ###### - - # returns false if user not allowed to 'show' Topic, Synapse, or Map - def authorize_to_show(user) - if (self.permission == "private" && self.user != user) - return false - end - return self - end - - # returns false if user not allowed to 'edit' Topic, Synapse, or Map - def authorize_to_edit(user) - if (self.permission == "private" && self.user != user) - return false - elsif (self.permission == "public" && self.user != user) - return false - end - return self - end - - def authorize_to_delete(user) - if (self.user == user || user.admin) - return self - end - return false - end end diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index 6bd56c64..39b7a961 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -39,7 +39,7 @@ class ApplicationPolicy # explicitly say they want to (E.g. seeing/editing/deleting private # maps - they should be able to, but not by accident) def admin_override - user.admin + user && user.admin end def scope diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 671eea83..5e845d44 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -1,7 +1,7 @@ class MapPolicy < ApplicationPolicy class Scope < Scope def resolve - scope.where('permission IN ("public", "commons") OR user_id = ?', user.id) + scope.where('maps.permission IN (?) OR maps.user_id = ?', ["public", "commons"], user.id) end end diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index 44e7bfd7..49e134ef 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -5,7 +5,7 @@ class MappingPolicy < ApplicationPolicy # it would be nice if we could also base this on the mappable, but that # gets really complicated. Devin thinks it's OK to SHOW a mapping for # a private topic, since you can't see the private topic anyways - scope.joins(:maps).where('maps.permission IN ("public", "commons") OR user_id = ?', user.id) + scope.joins(:maps).where('maps.permission IN ("public", "commons") OR maps.user_id = ?', user.id) end end From 77d39d664999935b6ac112e6e1c2adfcef05e4f9 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 09:48:07 +1100 Subject: [PATCH 215/305] redid so they won't interfere --- .../{map_serializer.rb => new_map_serializer.rb} | 10 +++++----- ...mapping_serializer.rb => new_mapping_serializer.rb} | 8 ++++---- ...tacode_serializer.rb => new_metacode_serializer.rb} | 2 +- ...synapse_serializer.rb => new_synapse_serializer.rb} | 8 ++++---- .../{topic_serializer.rb => new_topic_serializer.rb} | 6 +++--- .../{user_serializer.rb => new_user_serializer.rb} | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) rename app/serializers/{map_serializer.rb => new_map_serializer.rb} (52%) rename app/serializers/{mapping_serializer.rb => new_mapping_serializer.rb} (61%) rename app/serializers/{metacode_serializer.rb => new_metacode_serializer.rb} (67%) rename app/serializers/{synapse_serializer.rb => new_synapse_serializer.rb} (56%) rename app/serializers/{topic_serializer.rb => new_topic_serializer.rb} (66%) rename app/serializers/{user_serializer.rb => new_user_serializer.rb} (80%) diff --git a/app/serializers/map_serializer.rb b/app/serializers/new_map_serializer.rb similarity index 52% rename from app/serializers/map_serializer.rb rename to app/serializers/new_map_serializer.rb index b9839782..1e27420b 100644 --- a/app/serializers/map_serializer.rb +++ b/app/serializers/new_map_serializer.rb @@ -1,4 +1,4 @@ -class MapSerializer < ActiveModel::Serializer +class NewMapSerializer < ActiveModel::Serializer embed :ids, include: true attributes :id, :name, @@ -8,10 +8,10 @@ class MapSerializer < ActiveModel::Serializer :created_at, :updated_at - has_many :topics - has_many :synapses - has_many :mappings - has_many :contributors, root: :users + has_many :topics, serializer: NewTopicSerializer + has_many :synapses, serializer: NewSynapseSerializer + has_many :mappings, serializer: NewMappingSerializer + has_many :contributors, root: :users, serializer: NewUserSerializer #def filter(keys) # keys.delete(:outcome_author) unless object.outcome_author.present? diff --git a/app/serializers/mapping_serializer.rb b/app/serializers/new_mapping_serializer.rb similarity index 61% rename from app/serializers/mapping_serializer.rb rename to app/serializers/new_mapping_serializer.rb index 400c35ba..9241305a 100644 --- a/app/serializers/mapping_serializer.rb +++ b/app/serializers/new_mapping_serializer.rb @@ -1,13 +1,13 @@ -class MappingSerializer < ActiveModel::Serializer +class NewMappingSerializer < ActiveModel::Serializer embed :ids, include: true attributes :id, :xloc, :yloc, :created_at, :updated_at - has_one :user - has_one :map - has_one :mappable, polymorphic: true + has_one :user, serializer: NewUserSerializer + has_one :map, serializer: NewMapSerializer + has_one :mappable, polymorphic: true ##? def filter(keys) keys.delete(:xloc) unless object.mappable_type == "Topic" diff --git a/app/serializers/metacode_serializer.rb b/app/serializers/new_metacode_serializer.rb similarity index 67% rename from app/serializers/metacode_serializer.rb rename to app/serializers/new_metacode_serializer.rb index 3918d274..e664e7ea 100644 --- a/app/serializers/metacode_serializer.rb +++ b/app/serializers/new_metacode_serializer.rb @@ -1,4 +1,4 @@ -class MetacodeSerializer < ActiveModel::Serializer +class NewMetacodeSerializer < ActiveModel::Serializer attributes :id, :name, :manual_icon, diff --git a/app/serializers/synapse_serializer.rb b/app/serializers/new_synapse_serializer.rb similarity index 56% rename from app/serializers/synapse_serializer.rb rename to app/serializers/new_synapse_serializer.rb index 77c3a159..e7cd9fd7 100644 --- a/app/serializers/synapse_serializer.rb +++ b/app/serializers/new_synapse_serializer.rb @@ -1,4 +1,4 @@ -class SynapseSerializer < ActiveModel::Serializer +class NewSynapseSerializer < ActiveModel::Serializer embed :ids, include: true attributes :id, :desc, @@ -8,9 +8,9 @@ class SynapseSerializer < ActiveModel::Serializer :created_at, :updated_at - has_one :topic1, root: :topics - has_one :topic2, root: :topics - has_one :user + has_one :topic1, root: :topics, serializer: NewTopicSerializer + has_one :topic2, root: :topics, serializer: NewTopicSerializer + has_one :user, serializer: NewUserSerializer #def filter(keys) # keys.delete(:outcome_author) unless object.outcome_author.present? diff --git a/app/serializers/topic_serializer.rb b/app/serializers/new_topic_serializer.rb similarity index 66% rename from app/serializers/topic_serializer.rb rename to app/serializers/new_topic_serializer.rb index 205b8178..d36f1db0 100644 --- a/app/serializers/topic_serializer.rb +++ b/app/serializers/new_topic_serializer.rb @@ -1,4 +1,4 @@ -class TopicSerializer < ActiveModel::Serializer +class NewTopicSerializer < ActiveModel::Serializer embed :ids, include: true attributes :id, :name, @@ -8,8 +8,8 @@ class TopicSerializer < ActiveModel::Serializer :created_at, :updated_at - has_one :user - has_one :metacode + has_one :user, serializer: NewUserSerializer + has_one :metacode, serializer: NewMetacodeSerializer #def filter(keys) # keys.delete(:outcome_author) unless object.outcome_author.present? diff --git a/app/serializers/user_serializer.rb b/app/serializers/new_user_serializer.rb similarity index 80% rename from app/serializers/user_serializer.rb rename to app/serializers/new_user_serializer.rb index b9a3b416..45be36b0 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/new_user_serializer.rb @@ -1,4 +1,4 @@ -class UserSerializer < ActiveModel::Serializer +class NewUserSerializer < ActiveModel::Serializer attributes :id, :name, :avatar, From 623b3c7ad7ae2b9c8a4703b812bcbc50e60b2306 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 09:54:23 +1100 Subject: [PATCH 216/305] can load maps --- app/views/layouts/application.html.erb | 2 +- app/views/maps/_mapinfobox.html.erb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index c09d227e..0480cb39 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -81,7 +81,7 @@ classes += controller_name == "maps" && action_name == "index" ? " explorePage" : "" if controller_name == "maps" && action_name == "show" classes += " mapPage" - if @map.authorize_to_edit(current_user) + if policy(@map).update? classes += " canEditMap" end if @map.permission == "commons" diff --git a/app/views/maps/_mapinfobox.html.erb b/app/views/maps/_mapinfobox.html.erb index 1fa6da02..ff90532c 100644 --- a/app/views/maps/_mapinfobox.html.erb +++ b/app/views/maps/_mapinfobox.html.erb @@ -4,7 +4,7 @@ #%>
    - <%= @map && @map.authorize_to_edit(user) ? " canEdit" : "" %> + <%= @map && policy(@map).update? ? " canEdit" : "" %> <%= @map && @map.permission != 'private' ? " shareable" : "" %>"> <% if @map %> @@ -41,7 +41,7 @@
    - <% if (authenticated? && @map.authorize_to_edit(user)) || (!authenticated? && @map.desc != "" && @map.desc != nil )%> + <% if (authenticated? && policy(@map).update?) || (!authenticated? && @map.desc != "" && @map.desc != nil )%> <%= best_in_place @map, :desc, :activator => "#mapInfoDesc", :as => :textarea, :placeholder => "Click to add description...", :class => 'best_in_place_desc' %> <% end %>
    From fc1a7fd23d2e10f6db4f1844694ad0c7114db27d Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 10:05:42 +1100 Subject: [PATCH 217/305] api: adjust serializers --- app/controllers/api/restful_controller.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/controllers/api/restful_controller.rb b/app/controllers/api/restful_controller.rb index ca7ff481..d6c544e2 100644 --- a/app/controllers/api/restful_controller.rb +++ b/app/controllers/api/restful_controller.rb @@ -17,6 +17,10 @@ class API::RestfulController < ActionController::Base private + def resource_serializer + "new_#{resource_name}_serializer".camelize.constantize + end + def accessible_records if current_user visible_records From 0095a8daf42b0b5dbf8a05e6e5ccdc53d4bb8ab0 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 10:06:00 +1100 Subject: [PATCH 218/305] pundit: syntax error --- app/controllers/topics_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 125005f9..e9239bac 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -24,7 +24,7 @@ class TopicsController < ApplicationController respond_to do |format| format.html { - @alltopics = ([@topic] + policy_scope(@topic.relatives) + @alltopics = ([@topic] + policy_scope(@topic.relatives)) @allsynapses = policy_scope(@topic.synapses) @allcreators = @alltopics.map(&:user).uniq From c7075dab48c80cdfede708d0d95ee7390d366f12 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 10:10:31 +1100 Subject: [PATCH 219/305] pundit: fix queries --- app/policies/mapping_policy.rb | 3 ++- app/policies/synapse_policy.rb | 2 +- app/policies/topic_policy.rb | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index 49e134ef..39dbd86a 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -5,7 +5,8 @@ class MappingPolicy < ApplicationPolicy # it would be nice if we could also base this on the mappable, but that # gets really complicated. Devin thinks it's OK to SHOW a mapping for # a private topic, since you can't see the private topic anyways - scope.joins(:maps).where('maps.permission IN ("public", "commons") OR maps.user_id = ?', user.id) + scope.joins(:maps).where('maps.permission IN (?) OR maps.user_id = ?', + ["public", "commons"], user.id) end end diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 6763014a..12f9c8ca 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -1,7 +1,7 @@ class SynapsePolicy < ApplicationPolicy class Scope < Scope def resolve - scope.where('permission IN ("public", "commons") OR user_id = ?', user.id) + scope.where('permission IN (?) OR user_id = ?', ["public", "commons"], user.id) end end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 03b42895..97fefdcc 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -1,7 +1,7 @@ class TopicPolicy < ApplicationPolicy class Scope < Scope def resolve - scope.where('permission IN ("public", "commons") OR user_id = ?', user.id) + scope.where('permission IN (?) OR user_id = ?', ["public", "commons"], user.id) end end From 09a7b336bf1200f4368852dc9e1535cc7c050983 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 10:13:22 +1100 Subject: [PATCH 220/305] pundit: exclude topic action --- app/controllers/topics_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index e9239bac..0d58d912 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -2,7 +2,7 @@ class TopicsController < ApplicationController include TopicsHelper before_action :require_user, only: [:create, :update, :destroy] - after_action :verify_authorized + after_action :verify_authorized, except: :autocomplete_topic respond_to :html, :js, :json From bef21341c6a1ae7cccd2f89705087661746c9c25 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 11:10:30 +1100 Subject: [PATCH 221/305] pundit: fixing up topics and synapses --- app/controllers/synapses_controller.rb | 8 ++++---- app/controllers/topics_controller.rb | 16 ++++++++-------- app/models/synapse.rb | 4 ++++ app/models/topic.rb | 7 +++++++ app/policies/synapse_policy.rb | 2 +- app/policies/topic_policy.rb | 2 +- 6 files changed, 25 insertions(+), 14 deletions(-) diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index f242ad38..4440872f 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -10,7 +10,7 @@ class SynapsesController < ApplicationController # GET /synapses/1.json def show @synapse = Synapse.find(params[:id]) - authorize! @synapse + authorize @synapse render json: @synapse end @@ -20,7 +20,7 @@ class SynapsesController < ApplicationController def create @synapse = Synapse.new(synapse_params) @synapse.desc = "" if @synapse.desc.nil? - authorize! @synapse + authorize @synapse respond_to do |format| if @synapse.save @@ -36,7 +36,7 @@ class SynapsesController < ApplicationController def update @synapse = Synapse.find(params[:id]) @synapse.desc = "" if @synapse.desc.nil? - authorize! @synapse + authorize @synapse respond_to do |format| if @synapse.update_attributes(synapse_params) @@ -50,7 +50,7 @@ class SynapsesController < ApplicationController # DELETE synapses/:id def destroy @synapse = Synapse.find(params[:id]) - authorize! @synapse + authorize @synapse @synapse.delete respond_to do |format| diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 0d58d912..1b1e9b3c 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -20,12 +20,12 @@ class TopicsController < ApplicationController # GET topics/:id def show @topic = Topic.find(params[:id]) - authorize! @topic + authorize @topic respond_to do |format| format.html { - @alltopics = ([@topic] + policy_scope(@topic.relatives)) - @allsynapses = policy_scope(@topic.synapses) + @alltopics = ([@topic] + policy_scope(Topic.relatives(@topic.id))) + @allsynapses = policy_scope(Synapse.for_topic(@topic.id)) @allcreators = @alltopics.map(&:user).uniq @allcreators += @allsynapses.map(&:user).uniq @@ -39,7 +39,7 @@ class TopicsController < ApplicationController # GET topics/:id/network def network @topic = Topic.find(params[:id]) - authorize! @topic + authorize @topic @alltopics = [@topic] + policy_scope(@topic.relatives) @allsynapses = policy_scope(@topic.synapses) @@ -83,7 +83,7 @@ class TopicsController < ApplicationController # GET topics/:id/relatives def relatives @topic = Topic.find(params[:id]) - authorize! @topic + authorize @topic topicsAlreadyHas = params[:network] ? params[:network].split(',').map(&:to_i) : [] @@ -117,7 +117,7 @@ class TopicsController < ApplicationController # POST /topics.json def create @topic = Topic.new(topic_params) - authorize! @topic + authorize @topic respond_to do |format| if @topic.save @@ -132,7 +132,7 @@ class TopicsController < ApplicationController # PUT /topics/1.json def update @topic = Topic.find(params[:id]) - authorize! @topic + authorize @topic respond_to do |format| if @topic.update_attributes(topic_params) @@ -146,7 +146,7 @@ class TopicsController < ApplicationController # DELETE topics/:id def destroy @topic = Topic.find(params[:id]) - authorize! @topic + authorize @topic @topic.delete respond_to do |format| diff --git a/app/models/synapse.rb b/app/models/synapse.rb index d545a25c..540376bb 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -16,6 +16,10 @@ class Synapse < ActiveRecord::Base validates :category, inclusion: { in: ['from-to', 'both'], allow_nil: true } + scope :for_topic, ->(topic_id = nil) { + where("node1_id = ? OR node2_id = ?", topic_id, topic_id) + } + # :nocov: def user_name user.name diff --git a/app/models/topic.rb b/app/models/topic.rb index 0039040e..0f312823 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -41,6 +41,13 @@ class Topic < ActiveRecord::Base belongs_to :metacode + scope :relatives, ->(topic_id = nil) { + includes(:synapses1) + .includes(:synapses2) + .where('synapses.node1_id = ? OR synapses.node2_id = ?', topic_id, topic_id) + .references(:synapses) + } + def user_name user.name end diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 12f9c8ca..85de12da 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -1,7 +1,7 @@ class SynapsePolicy < ApplicationPolicy class Scope < Scope def resolve - scope.where('permission IN (?) OR user_id = ?', ["public", "commons"], user.id) + scope.where('synapses.permission IN (?) OR synapses.user_id = ?', ["public", "commons"], user.id) end end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 97fefdcc..43d4ec98 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -1,7 +1,7 @@ class TopicPolicy < ApplicationPolicy class Scope < Scope def resolve - scope.where('permission IN (?) OR user_id = ?', ["public", "commons"], user.id) + scope.where('topics.permission IN (?) OR topics.user_id = ?', ["public", "commons"], user.id) end end From 4ed00240ebff82816943f4fd82e6ce722cd66685 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 11:15:14 +1100 Subject: [PATCH 222/305] api: revert silly js change --- app/assets/javascripts/src/Metamaps.js.erb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index 95cf7379..ff6d57e6 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -4083,7 +4083,7 @@ Metamaps.Topic = { }; var topicSuccessCallback = function (topicModel, response) { if (Metamaps.Active.Map) { - mapping.save({ mappable_id: topicModel.get('topic').id }, { + mapping.save({ mappable_id: topicModel.id }, { success: mappingSuccessCallback, error: function (model, response) { console.log('error saving mapping to database'); @@ -4254,7 +4254,7 @@ Metamaps.Synapse = { }; var synapseSuccessCallback = function (synapseModel, response) { if (Metamaps.Active.Map) { - mapping.save({ mappable_id: synapseModel.get('synapse').id }, { + mapping.save({ mappable_id: synapseModel.id }, { success: mappingSuccessCallback }); } From cb79f2deae4aad86eb2c4b6bd89b02b27b62cc00 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 11:16:46 +1100 Subject: [PATCH 223/305] pundit: make it work --- app/controllers/mappings_controller.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index 5b64efe1..897d1ee1 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -9,7 +9,7 @@ class MappingsController < ApplicationController # GET /mappings/1.json def show @mapping = Mapping.find(params[:id]) - authorize! @mapping + authorize @mapping render json: @mapping end @@ -17,8 +17,8 @@ class MappingsController < ApplicationController # POST /mappings.json def create @mapping = Mapping.new(mapping_params) - authorize! @mapping - + authorize @mapping + @mapping.user = current_user if @mapping.save render json: @mapping, status: :created else @@ -29,7 +29,7 @@ class MappingsController < ApplicationController # PUT /mappings/1.json def update @mapping = Mapping.find(params[:id]) - authorize! @mapping + authorize @mapping if @mapping.update_attributes(mapping_params) head :no_content @@ -41,7 +41,7 @@ class MappingsController < ApplicationController # DELETE /mappings/1.json def destroy @mapping = Mapping.find(params[:id]) - authorize! @mapping + authorize @mapping @mapping.destroy @@ -51,6 +51,6 @@ class MappingsController < ApplicationController private # Never trust parameters from the scary internet, only allow the white list through. def mapping_params - params.require(:mapping).permit(:id, :xloc, :yloc, :mappable_id, :mappable_type, :map_id, :user_id) + params.require(:mapping).permit(:id, :xloc, :yloc, :mappable_id, :mappable_type, :map_id) end end From fdd93513785fff6334aaffdac161a2c078a1277a Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 11:24:49 +1100 Subject: [PATCH 224/305] pundit: policy didn't exist --- app/policies/mapping_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index 39dbd86a..13ef033e 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -18,7 +18,7 @@ class MappingPolicy < ApplicationPolicy def create? map = policy(record.map, user) - map.edit? + map.update? end def update? From d0fd676aa01db64496d0322923ae0221877430f4 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 11:35:03 +1100 Subject: [PATCH 225/305] pundit: now updating maps actually works --- app/policies/map_policy.rb | 1 - app/policies/mapping_policy.rb | 10 ++++------ app/policies/synapse_policy.rb | 1 + 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 5e845d44..50123877 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -35,7 +35,6 @@ class MapPolicy < ApplicationPolicy def update? user.present? && (record.permission == 'commons' || record.user == user) - true end def screenshot? diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index 13ef033e..787b5794 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -11,19 +11,17 @@ class MappingPolicy < ApplicationPolicy end def show? - map = policy(record.map, user) - mappable = policy(record.mappable, user) + map = Pundit.policy(user, record.map) + mappable = Pundit.policy(user, record.mappable) map.show? && mappable.show? end def create? - map = policy(record.map, user) - map.update? + Pundit.policy(user, record.map).update? end def update? - map = policy(record.map, user) - map.update? + Pundit.policy(user, record.map).update? end def destroy? diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 85de12da..e8d49548 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -7,6 +7,7 @@ class SynapsePolicy < ApplicationPolicy def create? user.present? + # todo add validation against whether you can see both topics end def show? From d8c328468ed318f38b93f39112276935343fe7c9 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 09:37:32 +1100 Subject: [PATCH 226/305] changess for pundit --- app/controllers/application_controller.rb | 1 + app/controllers/main_controller.rb | 4 +-- app/controllers/maps_controller.rb | 36 ++++++++--------------- app/models/map.rb | 29 ------------------ app/models/synapse.rb | 26 ---------------- app/models/topic.rb | 27 ----------------- app/policies/application_policy.rb | 2 +- app/policies/map_policy.rb | 2 +- app/policies/mapping_policy.rb | 2 +- 9 files changed, 19 insertions(+), 110 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6d10c553..d030c6e6 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,5 +1,6 @@ class ApplicationController < ActionController::Base include Pundit + include PunditExtra rescue_from Pundit::NotAuthorizedError, with: :handle_unauthorized protect_from_forgery diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 29c11777..efebdce7 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -4,8 +4,8 @@ class MainController < ApplicationController include UsersHelper include SynapsesHelper - after_action :verify_authorized, except: :index - after_action :verify_policy_scoped, only: :index +# after_action :verify_authorized, except: :index +# after_action :verify_policy_scoped, only: :index respond_to :html, :json diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 016ba7b5..83f7cb89 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,7 +1,7 @@ class MapsController < ApplicationController before_action :require_user, only: [:create, :update, :screenshot, :destroy] - after_action :verify_authorized, except: :activemaps, :featuredmaps, :mymaps, :usermaps - after_action :verify_policy_scoped, only: :activemaps, :featuredmaps, :mymaps, :usermaps + after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :usermaps] + after_action :verify_policy_scoped, only: [:activemaps, :featuredmaps, :mymaps, :usermaps] respond_to :html, :json @@ -67,11 +67,7 @@ class MapsController < ApplicationController # GET maps/:id def show @map = Map.find(params[:id]) - authorize! @map - - if not @map - redirect_to root_url, notice: "Access denied. That map is private." and return - end + authorize @map respond_to do |format| format.html { @@ -85,18 +81,14 @@ class MapsController < ApplicationController respond_with(@allmappers, @allmappings, @allsynapses, @alltopics, @map) } - format.json { render json: @map } + format.json { render json: @map.as_json } end end # GET maps/:id/contains def contains @map = Map.find(params[:id]) - authorize! @map - - if not @map - redirect_to root_url, notice: "Access denied. That map is private." and return - end + authorize @map @allmappers = @map.contributors @alltopics = @map.topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id)) } @@ -139,7 +131,7 @@ class MapsController < ApplicationController mapping.xloc = topic[1] mapping.yloc = topic[2] @map.topicmappings << mapping - authorize! mapping, :create + authorize mapping, :create mapping.save end @@ -152,7 +144,7 @@ class MapsController < ApplicationController mapping.map = @map mapping.mappable = Synapse.find(synapse_id) @map.synapsemappings << mapping - authorize! mapping, :create + authorize mapping, :create mapping.save end end @@ -160,7 +152,7 @@ class MapsController < ApplicationController @map.arranged = true end - authorize! @map + authorize @map if @map.save respond_to do |format| @@ -176,12 +168,10 @@ class MapsController < ApplicationController # PUT maps/:id def update @map = Map.find(params[:id]) - authorize! @map + authorize @map respond_to do |format| - if !@map - format.json { render json: "unauthorized" } - elsif @map.update_attributes(map_params) + if @map.update_attributes(map_params) format.json { head :no_content } else format.json { render json: @map.errors, status: :unprocessable_entity } @@ -192,7 +182,7 @@ class MapsController < ApplicationController # POST maps/:id/upload_screenshot def screenshot @map = Map.find(params[:id]) - authorize! @map + authorize @map png = Base64.decode64(params[:encoded_image]['data:image/png;base64,'.length .. -1]) StringIO.open(png) do |data| @@ -212,7 +202,7 @@ class MapsController < ApplicationController # DELETE maps/:id def destroy @map = Map.find(params[:id]) - authorize! @map + authorize @map @map.delete @@ -227,6 +217,6 @@ class MapsController < ApplicationController # Never trust parameters from the scary internet, only allow the white list through. def map_params - params.require(:map).permit(:id, :name, :arranged, :desc, :permission, :user_id) + params.require(:map).permit(:id, :name, :arranged, :desc, :permission) end end diff --git a/app/models/map.rb b/app/models/map.rb index 6c2caca2..87c8d641 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -78,36 +78,7 @@ class Map < ActiveRecord::Base json[:updated_at_clean] = updated_at_str json end - - ##### PERMISSIONS ###### - def authorize_to_delete(user) - if (self.user != user) - return false - end - return self - end - - # returns false if user not allowed to 'show' Topic, Synapse, or Map - def authorize_to_show(user) - if (self.permission == "private" && self.user != user) - return false - end - return self - end - - # returns false if user not allowed to 'edit' Topic, Synapse, or Map - def authorize_to_edit(user) - if !user - return false - elsif (self.permission == "private" && self.user != user) - return false - elsif (self.permission == "public" && self.user != user) - return false - end - return self - end - def decode_base64(imgBase64) decoded_data = Base64.decode64(imgBase64) diff --git a/app/models/synapse.rb b/app/models/synapse.rb index ea5889cc..beda8976 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -32,30 +32,4 @@ class Synapse < ActiveRecord::Base end # :nocov: - ##### PERMISSIONS ###### - - # returns false if user not allowed to 'show' Topic, Synapse, or Map - def authorize_to_show(user) - if (self.permission == "private" && self.user != user) - return false - end - return self - end - - # returns false if user not allowed to 'edit' Topic, Synapse, or Map - def authorize_to_edit(user) - if (self.permission == "private" && self.user != user) - return false - elsif (self.permission == "public" && self.user != user) - return false - end - return self - end - - def authorize_to_delete(user) - if (self.user == user || user.admin) - return self - end - return false - end end diff --git a/app/models/topic.rb b/app/models/topic.rb index c528aa6e..0039040e 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -87,31 +87,4 @@ class Topic < ActiveRecord::Base end result end - - ##### PERMISSIONS ###### - - # returns false if user not allowed to 'show' Topic, Synapse, or Map - def authorize_to_show(user) - if (self.permission == "private" && self.user != user) - return false - end - return self - end - - # returns false if user not allowed to 'edit' Topic, Synapse, or Map - def authorize_to_edit(user) - if (self.permission == "private" && self.user != user) - return false - elsif (self.permission == "public" && self.user != user) - return false - end - return self - end - - def authorize_to_delete(user) - if (self.user == user || user.admin) - return self - end - return false - end end diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index 6bd56c64..39b7a961 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -39,7 +39,7 @@ class ApplicationPolicy # explicitly say they want to (E.g. seeing/editing/deleting private # maps - they should be able to, but not by accident) def admin_override - user.admin + user && user.admin end def scope diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 671eea83..5e845d44 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -1,7 +1,7 @@ class MapPolicy < ApplicationPolicy class Scope < Scope def resolve - scope.where('permission IN ("public", "commons") OR user_id = ?', user.id) + scope.where('maps.permission IN (?) OR maps.user_id = ?', ["public", "commons"], user.id) end end diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index 44e7bfd7..49e134ef 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -5,7 +5,7 @@ class MappingPolicy < ApplicationPolicy # it would be nice if we could also base this on the mappable, but that # gets really complicated. Devin thinks it's OK to SHOW a mapping for # a private topic, since you can't see the private topic anyways - scope.joins(:maps).where('maps.permission IN ("public", "commons") OR user_id = ?', user.id) + scope.joins(:maps).where('maps.permission IN ("public", "commons") OR maps.user_id = ?', user.id) end end From 2d53922f1c4173131f5131ae9687f99b7dd4f988 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 09:54:23 +1100 Subject: [PATCH 227/305] can load maps --- app/views/layouts/application.html.erb | 2 +- app/views/maps/_mapinfobox.html.erb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index c09d227e..0480cb39 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -81,7 +81,7 @@ classes += controller_name == "maps" && action_name == "index" ? " explorePage" : "" if controller_name == "maps" && action_name == "show" classes += " mapPage" - if @map.authorize_to_edit(current_user) + if policy(@map).update? classes += " canEditMap" end if @map.permission == "commons" diff --git a/app/views/maps/_mapinfobox.html.erb b/app/views/maps/_mapinfobox.html.erb index 1fa6da02..ff90532c 100644 --- a/app/views/maps/_mapinfobox.html.erb +++ b/app/views/maps/_mapinfobox.html.erb @@ -4,7 +4,7 @@ #%>
    - <%= @map && @map.authorize_to_edit(user) ? " canEdit" : "" %> + <%= @map && policy(@map).update? ? " canEdit" : "" %> <%= @map && @map.permission != 'private' ? " shareable" : "" %>"> <% if @map %> @@ -41,7 +41,7 @@
    - <% if (authenticated? && @map.authorize_to_edit(user)) || (!authenticated? && @map.desc != "" && @map.desc != nil )%> + <% if (authenticated? && policy(@map).update?) || (!authenticated? && @map.desc != "" && @map.desc != nil )%> <%= best_in_place @map, :desc, :activator => "#mapInfoDesc", :as => :textarea, :placeholder => "Click to add description...", :class => 'best_in_place_desc' %> <% end %>
    From 5f3f5212c5890a14d665c0709f33cfbed988d04b Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 10:06:00 +1100 Subject: [PATCH 228/305] pundit: syntax error --- app/controllers/topics_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 125005f9..e9239bac 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -24,7 +24,7 @@ class TopicsController < ApplicationController respond_to do |format| format.html { - @alltopics = ([@topic] + policy_scope(@topic.relatives) + @alltopics = ([@topic] + policy_scope(@topic.relatives)) @allsynapses = policy_scope(@topic.synapses) @allcreators = @alltopics.map(&:user).uniq From bd3afff06911576214ff2465bfb02e9e66c05810 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 10:10:31 +1100 Subject: [PATCH 229/305] pundit: fix queries --- app/policies/mapping_policy.rb | 3 ++- app/policies/synapse_policy.rb | 2 +- app/policies/topic_policy.rb | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index 49e134ef..39dbd86a 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -5,7 +5,8 @@ class MappingPolicy < ApplicationPolicy # it would be nice if we could also base this on the mappable, but that # gets really complicated. Devin thinks it's OK to SHOW a mapping for # a private topic, since you can't see the private topic anyways - scope.joins(:maps).where('maps.permission IN ("public", "commons") OR maps.user_id = ?', user.id) + scope.joins(:maps).where('maps.permission IN (?) OR maps.user_id = ?', + ["public", "commons"], user.id) end end diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 6763014a..12f9c8ca 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -1,7 +1,7 @@ class SynapsePolicy < ApplicationPolicy class Scope < Scope def resolve - scope.where('permission IN ("public", "commons") OR user_id = ?', user.id) + scope.where('permission IN (?) OR user_id = ?', ["public", "commons"], user.id) end end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 03b42895..97fefdcc 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -1,7 +1,7 @@ class TopicPolicy < ApplicationPolicy class Scope < Scope def resolve - scope.where('permission IN ("public", "commons") OR user_id = ?', user.id) + scope.where('permission IN (?) OR user_id = ?', ["public", "commons"], user.id) end end From 1cf3182e7555cdb9c75253e1ca54fecbe47f1517 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 10:13:22 +1100 Subject: [PATCH 230/305] pundit: exclude topic action --- app/controllers/topics_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index e9239bac..0d58d912 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -2,7 +2,7 @@ class TopicsController < ApplicationController include TopicsHelper before_action :require_user, only: [:create, :update, :destroy] - after_action :verify_authorized + after_action :verify_authorized, except: :autocomplete_topic respond_to :html, :js, :json From dc6ccd20223709c087d7ef6784772cfd597ee381 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 11:10:30 +1100 Subject: [PATCH 231/305] pundit: fixing up topics and synapses --- app/controllers/synapses_controller.rb | 8 ++++---- app/controllers/topics_controller.rb | 16 ++++++++-------- app/models/synapse.rb | 4 ++++ app/models/topic.rb | 7 +++++++ app/policies/synapse_policy.rb | 2 +- app/policies/topic_policy.rb | 2 +- 6 files changed, 25 insertions(+), 14 deletions(-) diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index f242ad38..4440872f 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -10,7 +10,7 @@ class SynapsesController < ApplicationController # GET /synapses/1.json def show @synapse = Synapse.find(params[:id]) - authorize! @synapse + authorize @synapse render json: @synapse end @@ -20,7 +20,7 @@ class SynapsesController < ApplicationController def create @synapse = Synapse.new(synapse_params) @synapse.desc = "" if @synapse.desc.nil? - authorize! @synapse + authorize @synapse respond_to do |format| if @synapse.save @@ -36,7 +36,7 @@ class SynapsesController < ApplicationController def update @synapse = Synapse.find(params[:id]) @synapse.desc = "" if @synapse.desc.nil? - authorize! @synapse + authorize @synapse respond_to do |format| if @synapse.update_attributes(synapse_params) @@ -50,7 +50,7 @@ class SynapsesController < ApplicationController # DELETE synapses/:id def destroy @synapse = Synapse.find(params[:id]) - authorize! @synapse + authorize @synapse @synapse.delete respond_to do |format| diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 0d58d912..1b1e9b3c 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -20,12 +20,12 @@ class TopicsController < ApplicationController # GET topics/:id def show @topic = Topic.find(params[:id]) - authorize! @topic + authorize @topic respond_to do |format| format.html { - @alltopics = ([@topic] + policy_scope(@topic.relatives)) - @allsynapses = policy_scope(@topic.synapses) + @alltopics = ([@topic] + policy_scope(Topic.relatives(@topic.id))) + @allsynapses = policy_scope(Synapse.for_topic(@topic.id)) @allcreators = @alltopics.map(&:user).uniq @allcreators += @allsynapses.map(&:user).uniq @@ -39,7 +39,7 @@ class TopicsController < ApplicationController # GET topics/:id/network def network @topic = Topic.find(params[:id]) - authorize! @topic + authorize @topic @alltopics = [@topic] + policy_scope(@topic.relatives) @allsynapses = policy_scope(@topic.synapses) @@ -83,7 +83,7 @@ class TopicsController < ApplicationController # GET topics/:id/relatives def relatives @topic = Topic.find(params[:id]) - authorize! @topic + authorize @topic topicsAlreadyHas = params[:network] ? params[:network].split(',').map(&:to_i) : [] @@ -117,7 +117,7 @@ class TopicsController < ApplicationController # POST /topics.json def create @topic = Topic.new(topic_params) - authorize! @topic + authorize @topic respond_to do |format| if @topic.save @@ -132,7 +132,7 @@ class TopicsController < ApplicationController # PUT /topics/1.json def update @topic = Topic.find(params[:id]) - authorize! @topic + authorize @topic respond_to do |format| if @topic.update_attributes(topic_params) @@ -146,7 +146,7 @@ class TopicsController < ApplicationController # DELETE topics/:id def destroy @topic = Topic.find(params[:id]) - authorize! @topic + authorize @topic @topic.delete respond_to do |format| diff --git a/app/models/synapse.rb b/app/models/synapse.rb index beda8976..4807d7ab 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -14,6 +14,10 @@ class Synapse < ActiveRecord::Base validates :category, inclusion: { in: ['from-to', 'both'], allow_nil: true } + scope :for_topic, ->(topic_id = nil) { + where("node1_id = ? OR node2_id = ?", topic_id, topic_id) + } + # :nocov: def user_name user.name diff --git a/app/models/topic.rb b/app/models/topic.rb index 0039040e..0f312823 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -41,6 +41,13 @@ class Topic < ActiveRecord::Base belongs_to :metacode + scope :relatives, ->(topic_id = nil) { + includes(:synapses1) + .includes(:synapses2) + .where('synapses.node1_id = ? OR synapses.node2_id = ?', topic_id, topic_id) + .references(:synapses) + } + def user_name user.name end diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 12f9c8ca..85de12da 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -1,7 +1,7 @@ class SynapsePolicy < ApplicationPolicy class Scope < Scope def resolve - scope.where('permission IN (?) OR user_id = ?', ["public", "commons"], user.id) + scope.where('synapses.permission IN (?) OR synapses.user_id = ?', ["public", "commons"], user.id) end end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 97fefdcc..43d4ec98 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -1,7 +1,7 @@ class TopicPolicy < ApplicationPolicy class Scope < Scope def resolve - scope.where('permission IN (?) OR user_id = ?', ["public", "commons"], user.id) + scope.where('topics.permission IN (?) OR topics.user_id = ?', ["public", "commons"], user.id) end end From d0aecc0b31cf94217a8930ef3f6839af1f3cc836 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 11:16:46 +1100 Subject: [PATCH 232/305] pundit: make it work --- app/controllers/mappings_controller.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index ea2aaf0e..cba1cb3f 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -8,7 +8,7 @@ class MappingsController < ApplicationController # GET /mappings/1.json def show @mapping = Mapping.find(params[:id]) - authorize! @mapping + authorize @mapping render json: @mapping end @@ -16,8 +16,8 @@ class MappingsController < ApplicationController # POST /mappings.json def create @mapping = Mapping.new(mapping_params) - authorize! @mapping - + authorize @mapping + @mapping.user = current_user if @mapping.save render json: @mapping, status: :created else @@ -28,7 +28,7 @@ class MappingsController < ApplicationController # PUT /mappings/1.json def update @mapping = Mapping.find(params[:id]) - authorize! @mapping + authorize @mapping if @mapping.update_attributes(mapping_params) head :no_content @@ -40,7 +40,7 @@ class MappingsController < ApplicationController # DELETE /mappings/1.json def destroy @mapping = Mapping.find(params[:id]) - authorize! @mapping + authorize @mapping @mapping.destroy @@ -50,6 +50,6 @@ class MappingsController < ApplicationController private # Never trust parameters from the scary internet, only allow the white list through. def mapping_params - params.require(:mapping).permit(:id, :xloc, :yloc, :mappable_id, :mappable_type, :map_id, :user_id) + params.require(:mapping).permit(:id, :xloc, :yloc, :mappable_id, :mappable_type, :map_id) end end From 5d179ae5ec822b635804642dd3443d9768f5ea95 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 11:24:49 +1100 Subject: [PATCH 233/305] pundit: policy didn't exist --- app/policies/mapping_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index 39dbd86a..13ef033e 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -18,7 +18,7 @@ class MappingPolicy < ApplicationPolicy def create? map = policy(record.map, user) - map.edit? + map.update? end def update? From bc505a13610b0a12a55c85ed0852c1448def7141 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 11:35:03 +1100 Subject: [PATCH 234/305] pundit: now updating maps actually works --- app/policies/map_policy.rb | 1 - app/policies/mapping_policy.rb | 10 ++++------ app/policies/synapse_policy.rb | 1 + 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 5e845d44..50123877 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -35,7 +35,6 @@ class MapPolicy < ApplicationPolicy def update? user.present? && (record.permission == 'commons' || record.user == user) - true end def screenshot? diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index 13ef033e..787b5794 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -11,19 +11,17 @@ class MappingPolicy < ApplicationPolicy end def show? - map = policy(record.map, user) - mappable = policy(record.mappable, user) + map = Pundit.policy(user, record.map) + mappable = Pundit.policy(user, record.mappable) map.show? && mappable.show? end def create? - map = policy(record.map, user) - map.update? + Pundit.policy(user, record.map).update? end def update? - map = policy(record.map, user) - map.update? + Pundit.policy(user, record.map).update? end def destroy? diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 85de12da..e8d49548 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -7,6 +7,7 @@ class SynapsePolicy < ApplicationPolicy def create? user.present? + # todo add validation against whether you can see both topics end def show? From 7f86810f625197e240a1ea51f975c767cdc5d03a Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 11:53:12 +1100 Subject: [PATCH 235/305] remove things again not needed --- app/models/mapping.rb | 8 -------- app/models/user.rb | 4 ---- 2 files changed, 12 deletions(-) diff --git a/app/models/mapping.rb b/app/models/mapping.rb index 8f29ed6a..1a37f490 100644 --- a/app/models/mapping.rb +++ b/app/models/mapping.rb @@ -26,12 +26,4 @@ class Mapping < ActiveRecord::Base super(:methods =>[:user_name, :user_image]) end - def authorize_to_show(user) - if ((self.map.permission == "private" && self.map.user != user) || - (self.mappable.permission == "private" && self.mappable.user != user)) - return false - end - return self - end - end diff --git a/app/models/user.rb b/app/models/user.rb index a8fb6e6b..50493a55 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -41,10 +41,6 @@ class User < ActiveRecord::Base # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :image, :content_type => /\Aimage\/.*\Z/ - def is_logged_in? - true - end - # override default as_json def as_json(options={}) { :id => self.id, From 521aa6b5d00cda63d6b64210cecc861bc52197a4 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 11:58:26 +1100 Subject: [PATCH 236/305] function no longer exists --- app/controllers/api/tokens_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/tokens_controller.rb b/app/controllers/api/tokens_controller.rb index 6ef01e69..481b41ba 100644 --- a/app/controllers/api/tokens_controller.rb +++ b/app/controllers/api/tokens_controller.rb @@ -3,7 +3,7 @@ class Api::TokensController < API::RestfulController skip_authorization def my_tokens - raise Pundit::NotAuthorizedError.new unless current_user.is_logged_in? + raise Pundit::NotAuthorizedError.new unless current_user instantiate_collection page_collection: false, timeframe_collection: false respond_with_collection end From bb03b49d800c7d4b424fdd9b9459d2210326b824 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 12 Mar 2016 09:09:41 +0800 Subject: [PATCH 237/305] update main controller (searching) to use policies --- app/controllers/main_controller.rb | 112 +++++++++---------------- app/controllers/mappings_controller.rb | 1 + 2 files changed, 39 insertions(+), 74 deletions(-) diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index efebdce7..363c9c94 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -4,14 +4,13 @@ class MainController < ApplicationController include UsersHelper include SynapsesHelper -# after_action :verify_authorized, except: :index -# after_action :verify_policy_scoped, only: :index + after_action :verify_policy_scoped respond_to :html, :json # home page def home - @maps = Map.where("maps.permission != ?", "private").order("updated_at DESC").page(1).per(20) + @maps = policy_scope(Map).order("updated_at DESC").page(1).per(20) respond_to do |format| format.html { if authenticated? @@ -60,69 +59,35 @@ class MainController < ApplicationController filterByMetacode = m end end + + search = '%' + term.downcase + '%' + builder = policy_scope(Topic) if filterByMetacode if term == "" - @topics = [] + builder = builder.none else - search = term.downcase + '%' - - if user - @topics = Set.new(Topic.where('LOWER("name") like ?', search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"')) - @topics2 = Set.new(Topic.where('LOWER("name") like ?', '%' + search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"')) - @topics3 = Set.new(Topic.where('LOWER("desc") like ?', '%' + search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"')) - @topics4 = Set.new(Topic.where('LOWER("link") like ?', '%' + search).where('metacode_id = ? AND user_id = ?', filterByMetacode.id, user).order('"name"')) - else - @topics = Set.new(Topic.where('LOWER("name") like ?', search).where('metacode_id = ?', filterByMetacode.id).order('"name"')) - @topics2 = Set.new(Topic.where('LOWER("name") like ?', '%' + search).where('metacode_id = ?', filterByMetacode.id).order('"name"')) - @topics3 = Set.new(Topic.where('LOWER("desc") like ?', '%' + search).where('metacode_id = ?', filterByMetacode.id).order('"name"')) - @topics4 = Set.new(Topic.where('LOWER("link") like ?', '%' + search).where('metacode_id = ?', filterByMetacode.id).order('"name"')) - end - - #get unique elements only through the magic of Sets - @topics = (@topics + @topics2 + @topics3 + @topics4).to_a + builder = builder.where('LOWER("name") like ? OR + LOWER("desc") like ? OR + LOWER("link") like ?', search, search, search) + builder = builder.where(metacode_id: filterByMetacode.id) end elsif desc - search = '%' + term.downcase + '%' - if !user - @topics = Topic.where('LOWER("desc") like ?', search).order('"name"') - elsif user - @topics = Topic.where('LOWER("desc") like ?', search).where('user_id = ?', user).order('"name"') - end + builder = builder.where('LOWER("desc") like ?', search) elsif link - search = '%' + term.downcase + '%' - if !user - @topics = Topic.where('LOWER("link") like ?', search).order('"name"') - elsif user - @topics = Topic.where('LOWER("link") like ?', search).where('user_id = ?', user).order('"name"') - end + builder = builder.where('LOWER("link") like ?', search) else #regular case, just search the name - search = term.downcase + '%' - if !user - @topics = Topic.where('LOWER("name") like ?', search).order('"name"') - @topics2 = Topic.where('LOWER("name") like ?', '%' + search).order('"name"') - @topics3 = Topic.where('LOWER("desc") like ?', '%' + search).order('"name"') - @topics4 = Topic.where('LOWER("link") like ?', '%' + search).order('"name"') - @topics = @topics + (@topics2 - @topics) - @topics = @topics + (@topics3 - @topics) - @topics = @topics + (@topics4 - @topics) - elsif user - @topics = Topic.where('LOWER("name") like ?', search).where('user_id = ?', user).order('"name"') - @topics2 = Topic.where('LOWER("name") like ?', '%' + search).where('user_id = ?', user).order('"name"') - @topics3 = Topic.where('LOWER("desc") like ?', '%' + search).where('user_id = ?', user).order('"name"') - @topics4 = Topic.where('LOWER("link") like ?', '%' + search).where('user_id = ?', user).order('"name"') - @topics = @topics + (@topics2 - @topics) - @topics = @topics + (@topics3 - @topics) - @topics = @topics + (@topics4 - @topics) - end + builder = builder.where('LOWER("name") like ? OR + LOWER("desc") like ? OR + LOWER("link") like ?', search, search, search) end + + builder = builder.where(user: user) if user + @topics = builder.order(:name) else @topics = [] end - - #read this next line as 'delete a topic if its private and you're either 1. logged out or 2. logged in but not the topic creator - @topics.to_a.delete_if {|t| t.permission == "private" && (!authenticated? || (authenticated? && current_user.id != t.user_id)) } - + render json: autocomplete_array_json(@topics) end @@ -142,21 +107,21 @@ class MainController < ApplicationController term = term[5..-1] desc = true end + search = '%' + term.downcase + '%' - query = desc ? 'LOWER("desc") like ?' : 'LOWER("name") like ?' - if !user - # !connor why is the limit 5 done here and not above? also, why not limit after sorting alphabetically? - @maps = Map.where(query, search).limit(5).order('"name"') - elsif user - @maps = Map.where(query, search).where('user_id = ?', user).order('"name"') + builder = policy_scope(Map) + + if desc + builder = builder.where('LOWER("desc") like ?', search) + else + builder = builder.where('LOWER("name") like ?', search) end + builder = builder.where(user: user) if user + @maps = builder.order(:name) else @maps = [] end - #read this next line as 'delete a map if its private and you're either 1. logged out or 2. logged in but not the map creator - @maps.to_a.delete_if {|m| m.permission == "private" && (!authenticated? || (authenticated? && current_user.id != m.user_id)) } - render json: autocomplete_map_array_json(@maps) end @@ -167,7 +132,10 @@ class MainController < ApplicationController #remove "mapper:" if appended at beginning term = term[7..-1] if term.downcase[0..6] == "mapper:" - @mappers = User.where('LOWER("name") like ?', term.downcase + '%').order('"name"') + search = term.downcase + '%' + builder = policy_scope(User) # TODO do I need to policy scope? I guess yes to verify_policy_scoped + builder = builder.where('LOWER("name") like ?', search) + @mappers = builder.order(:name) else @mappers = [] end @@ -182,7 +150,7 @@ class MainController < ApplicationController topic2id = params[:topic2id] if term && !term.empty? - @synapses = Synapse.where('LOWER("desc") like ?', '%' + term.downcase + '%').order('"desc"') + @synapses = policy_scope(Synapse).where('LOWER("desc") like ?', '%' + term.downcase + '%').order('"desc"') # remove any duplicate synapse types that just differ by # leading or trailing whitespaces @@ -196,22 +164,18 @@ class MainController < ApplicationController boolean = true end } - - #limit to 5 results - @synapses = @synapses.slice(0,5) elsif topic1id && !topic1id.empty? - @one = Synapse.where('node1_id = ? AND node2_id = ?', topic1id, topic2id) - @two = Synapse.where('node2_id = ? AND node1_id = ?', topic1id, topic2id) + @one = policy_scope(Synapse).where('node1_id = ? AND node2_id = ?', topic1id, topic2id) + @two = policy_scope(Synapse).where('node2_id = ? AND node1_id = ?', topic1id, topic2id) @synapses = @one + @two @synapses.sort! {|s1,s2| s1.desc <=> s2.desc }.to_a - - #permissions - @synapses.delete_if {|s| s.permission == "private" && !authenticated? } - @synapses.delete_if {|s| s.permission == "private" && authenticated? && current_user.id != s.user_id } else @synapses = [] end + #limit to 5 results + @synapses = @synapses.slice(0,5) + render json: autocomplete_synapse_array_json(@synapses) end end diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index cba1cb3f..02c28a54 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -18,6 +18,7 @@ class MappingsController < ApplicationController @mapping = Mapping.new(mapping_params) authorize @mapping @mapping.user = current_user + if @mapping.save render json: @mapping, status: :created else From bf4fbbeb06439ed488c4776e8b4ba429a906858e Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 12:26:23 +1100 Subject: [PATCH 238/305] fix tokens --- app/controllers/api/restful_controller.rb | 2 +- app/controllers/api/tokens_controller.rb | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/restful_controller.rb b/app/controllers/api/restful_controller.rb index d6c544e2..06396e3d 100644 --- a/app/controllers/api/restful_controller.rb +++ b/app/controllers/api/restful_controller.rb @@ -5,7 +5,7 @@ class API::RestfulController < ActionController::Base snorlax_used_rest! rescue_from(Pundit::NotAuthorizedError) { |e| respond_with_standard_error e, 403 } - load_and_authorize_resource except: [:index, :create] + load_and_authorize_resource only: [:show, :update, :destroy] def create authorize resource_class diff --git a/app/controllers/api/tokens_controller.rb b/app/controllers/api/tokens_controller.rb index 481b41ba..3fcca370 100644 --- a/app/controllers/api/tokens_controller.rb +++ b/app/controllers/api/tokens_controller.rb @@ -1,13 +1,17 @@ class Api::TokensController < API::RestfulController - skip_authorization - def my_tokens raise Pundit::NotAuthorizedError.new unless current_user instantiate_collection page_collection: false, timeframe_collection: false respond_with_collection end + private + + def resource_serializer + "#{resource_name}_serializer".camelize.constantize + end + def visible_records current_user.tokens end From 7e7ef173e501301d659105af1e47081d98ff0cbe Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 12 Mar 2016 09:27:31 +0800 Subject: [PATCH 239/305] map policy spec --- spec/policies/map_policy_spec.rb | 97 ++++++++++++++++++++++++++++++++ spec/spec_helper.rb | 1 + 2 files changed, 98 insertions(+) create mode 100644 spec/policies/map_policy_spec.rb diff --git a/spec/policies/map_policy_spec.rb b/spec/policies/map_policy_spec.rb new file mode 100644 index 00000000..b160fead --- /dev/null +++ b/spec/policies/map_policy_spec.rb @@ -0,0 +1,97 @@ +require 'rails_helper' + +RSpec.describe MapPolicy, type: :policy do + subject { described_class } + + context 'unauthenticated' do + context 'commons' do + let(:map) { create(:map, permission: :commons) } + permissions :show? do + it 'can view' do + expect(subject).to permit(nil, map) + end + end + permissions :create?, :update?, :destroy? do + it 'can not modify' do + expect(subject).to_not permit(nil, map) + end + end + end + + context 'private' do + let(:map) { create(:map, permission: :private) } + permissions :show?, :create?, :update?, :destroy? do + it 'can not view or modify' do + expect(subject).to_not permit(nil, map) + end + end + end + end + + # + # Now begin the logged-in tests + # + + context 'logged in' do + let(:user) { create(:user) } + + context 'commons' do + let(:owner) { create(:user) } + let(:map) { create(:map, permission: :commons, user: owner) } + permissions :show?, :create?, :update? do + it 'can view and modify' do + expect(subject).to permit(user, map) + end + end + permissions :destroy? do + it 'can not destroy' do + expect(subject).to_not permit(user, map) + end + it 'owner can destroy' do + expect(subject).to permit(owner, map) + end + end + end + + context 'public' do + let(:owner) { create(:user) } + let(:map) { create(:map, permission: :public, user: owner) } + permissions :show? do + it 'can view' do + expect(subject).to permit(user, map) + end + end + permissions :create? do + it 'can create' do + expect(subject).to permit(user, map) + end + end + permissions :update?, :destroy? do + it 'can not update/destroy' do + expect(subject).to_not permit(user, map) + end + it 'owner can update/destroy' do + expect(subject).to permit(owner, map) + end + end + end + + context 'private' do + let(:owner) { create(:user) } + let(:map) { create(:map, permission: :private, user: owner) } + permissions :create? do + it 'can create' do + expect(subject).to permit(user, map) + end + end + permissions :show?, :update?, :destroy? do + it 'can not view or modify' do + expect(subject).to_not permit(user, map) + end + it 'owner can view and modify' do + expect(subject).to permit(owner, map) + end + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 08ffd17f..d4028602 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,6 @@ require 'simplecov' require 'support/controller_helpers' +require 'pundit/rspec' RSpec.configure do |config| config.expect_with :rspec do |expectations| From a295c61322efdb0384ec3b9203b655321eb7bdca Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 12:58:13 +1100 Subject: [PATCH 240/305] json response was broken --- app/controllers/main_controller.rb | 2 +- app/controllers/maps_controller.rb | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 363c9c94..7bc22c7e 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -13,7 +13,7 @@ class MainController < ApplicationController @maps = policy_scope(Map).order("updated_at DESC").page(1).per(20) respond_to do |format| format.html { - if authenticated? + if not authenticated? render 'main/home' else render 'maps/activemaps' diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 83f7cb89..d45c1432 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -13,11 +13,12 @@ class MapsController < ApplicationController @maps = policy_scope(Map).order("updated_at DESC") .page(page).per(20) - # root url => main/home. main/home renders maps/activemaps view. - redirect_to root_url and return if authenticated? - respond_to do |format| - format.html { respond_with(@maps, @user) } + format.html { + # root url => main/home. main/home renders maps/activemaps view. + redirect_to root_url and return if authenticated? + respond_with(@maps, @user) + } format.json { render json: @maps } end end @@ -81,7 +82,7 @@ class MapsController < ApplicationController respond_with(@allmappers, @allmappings, @allsynapses, @alltopics, @map) } - format.json { render json: @map.as_json } + format.json { render json: @map } end end From 446619c451b529b29f1fc62e14e215d27861f73e Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 13:20:15 +1100 Subject: [PATCH 241/305] omg not having this was breaking things --- app/controllers/application_controller.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d030c6e6..4388337e 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -7,6 +7,10 @@ class ApplicationController < ActionController::Base before_action :get_invite_link after_action :allow_embedding + def default_serializer_options + { root: false } + end + # this is for global login include ContentHelper From ada29c6832fee8629dbed804c0af7ee911859ab4 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 12 Mar 2016 21:35:56 +1100 Subject: [PATCH 242/305] cleanup --- app/serializers/new_map_serializer.rb | 4 ---- app/serializers/new_synapse_serializer.rb | 4 ---- app/serializers/new_topic_serializer.rb | 4 ---- app/serializers/token_serializer.rb | 4 ---- postatoken.txt | 7 ------- postdata.txt | 1 - postmapping.txt | 1 - postsynapse.txt | 1 - posttopic.txt | 1 - 9 files changed, 27 deletions(-) delete mode 100644 postatoken.txt delete mode 100644 postdata.txt delete mode 100644 postmapping.txt delete mode 100644 postsynapse.txt delete mode 100644 posttopic.txt diff --git a/app/serializers/new_map_serializer.rb b/app/serializers/new_map_serializer.rb index 1e27420b..9b2ff400 100644 --- a/app/serializers/new_map_serializer.rb +++ b/app/serializers/new_map_serializer.rb @@ -13,8 +13,4 @@ class NewMapSerializer < ActiveModel::Serializer has_many :mappings, serializer: NewMappingSerializer has_many :contributors, root: :users, serializer: NewUserSerializer - #def filter(keys) - # keys.delete(:outcome_author) unless object.outcome_author.present? - # keys - #end end diff --git a/app/serializers/new_synapse_serializer.rb b/app/serializers/new_synapse_serializer.rb index e7cd9fd7..b145d605 100644 --- a/app/serializers/new_synapse_serializer.rb +++ b/app/serializers/new_synapse_serializer.rb @@ -12,8 +12,4 @@ class NewSynapseSerializer < ActiveModel::Serializer has_one :topic2, root: :topics, serializer: NewTopicSerializer has_one :user, serializer: NewUserSerializer - #def filter(keys) - # keys.delete(:outcome_author) unless object.outcome_author.present? - # keys - #end end diff --git a/app/serializers/new_topic_serializer.rb b/app/serializers/new_topic_serializer.rb index d36f1db0..cdbbedf9 100644 --- a/app/serializers/new_topic_serializer.rb +++ b/app/serializers/new_topic_serializer.rb @@ -11,8 +11,4 @@ class NewTopicSerializer < ActiveModel::Serializer has_one :user, serializer: NewUserSerializer has_one :metacode, serializer: NewMetacodeSerializer - #def filter(keys) - # keys.delete(:outcome_author) unless object.outcome_author.present? - # keys - #end end diff --git a/app/serializers/token_serializer.rb b/app/serializers/token_serializer.rb index 777a14c1..8eed535a 100644 --- a/app/serializers/token_serializer.rb +++ b/app/serializers/token_serializer.rb @@ -7,8 +7,4 @@ class TokenSerializer < ActiveModel::Serializer :created_at, :updated_at - #def filter(keys) - # keys.delete(:outcome_author) unless object.outcome_author.present? - # keys - #end end diff --git a/postatoken.txt b/postatoken.txt deleted file mode 100644 index c9d98ff3..00000000 --- a/postatoken.txt +++ /dev/null @@ -1,7 +0,0 @@ -$.post('http://localhost:3000/api/v1/tokens', {token: { - description: 'for stuff', - token: '1234', - user_id: 2 -}}) - -curl -X POST -d @postdata.txt http://localhost:3000/api/v1/maps --header "Authorization: Token token=fb5b3db125c94e9fb50f1e42054be856" --header "Content-Type:application/json" diff --git a/postdata.txt b/postdata.txt deleted file mode 100644 index 9daaf598..00000000 --- a/postdata.txt +++ /dev/null @@ -1 +0,0 @@ -{ "map": { "name":"tree", "desc": "green", "permission": "commons", "arranged": true }} diff --git a/postmapping.txt b/postmapping.txt deleted file mode 100644 index c9463f92..00000000 --- a/postmapping.txt +++ /dev/null @@ -1 +0,0 @@ -{ "mapping": { "xloc": 123, "yloc": 123, "map_id": 1, "mappable_type": "Topic", "mappable_id": 2 }} diff --git a/postsynapse.txt b/postsynapse.txt deleted file mode 100644 index da2617d8..00000000 --- a/postsynapse.txt +++ /dev/null @@ -1 +0,0 @@ -{ "synapse": { "desc": "link between", "permission": "commons", "node1_id": 1, "node2_id": 2, "category": "from-to" }} diff --git a/posttopic.txt b/posttopic.txt deleted file mode 100644 index e00d797f..00000000 --- a/posttopic.txt +++ /dev/null @@ -1 +0,0 @@ -{ "topic": { "name":"tree topic", "desc": "so green", "permission": "commons", "metacode_id": 1 }} From f072e39c4c19c6d466f38853a2e6dad0d953d731 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 13 Mar 2016 02:27:05 +1100 Subject: [PATCH 243/305] pundit: sometimes no user --- app/policies/map_policy.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 50123877..1594e6d9 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -1,7 +1,11 @@ class MapPolicy < ApplicationPolicy class Scope < Scope def resolve - scope.where('maps.permission IN (?) OR maps.user_id = ?', ["public", "commons"], user.id) + if user + scope.where('maps.permission IN (?) OR maps.user_id = ?', ["public", "commons"], user.id) + else + scope.where('maps.permission IN (?)', ["public", "commons"]) + end end end From 3aec00e07cfe97eb3616d157148b422195671de4 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 13 Mar 2016 02:28:39 +1100 Subject: [PATCH 244/305] just don't include mappable for now --- app/serializers/new_mapping_serializer.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/serializers/new_mapping_serializer.rb b/app/serializers/new_mapping_serializer.rb index 9241305a..4e65e161 100644 --- a/app/serializers/new_mapping_serializer.rb +++ b/app/serializers/new_mapping_serializer.rb @@ -4,10 +4,12 @@ class NewMappingSerializer < ActiveModel::Serializer :xloc, :yloc, :created_at, - :updated_at + :updated_at, + :mappable_id, + :mappable_type + has_one :user, serializer: NewUserSerializer has_one :map, serializer: NewMapSerializer - has_one :mappable, polymorphic: true ##? def filter(keys) keys.delete(:xloc) unless object.mappable_type == "Topic" From b236f4c689acff80cbc98e2f615419bf277d1bd3 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 13 Mar 2016 02:41:32 +1100 Subject: [PATCH 245/305] handle not logged in scenarios --- app/policies/map_policy.rb | 6 ++++-- app/policies/mapping_policy.rb | 9 +++++++-- app/policies/synapse_policy.rb | 8 +++++++- app/policies/topic_policy.rb | 8 +++++++- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 1594e6d9..65f721bf 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -1,10 +1,12 @@ class MapPolicy < ApplicationPolicy class Scope < Scope def resolve + visible = ['public', 'commons'] + permission = 'maps.permission IN (?)' if user - scope.where('maps.permission IN (?) OR maps.user_id = ?', ["public", "commons"], user.id) + scope.where(permission + ' OR maps.user_id = ?', visible, user.id) else - scope.where('maps.permission IN (?)', ["public", "commons"]) + scope.where(permission, visible) end end end diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index 787b5794..ed93bc66 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -5,8 +5,13 @@ class MappingPolicy < ApplicationPolicy # it would be nice if we could also base this on the mappable, but that # gets really complicated. Devin thinks it's OK to SHOW a mapping for # a private topic, since you can't see the private topic anyways - scope.joins(:maps).where('maps.permission IN (?) OR maps.user_id = ?', - ["public", "commons"], user.id) + visible = ['public', 'commons'] + permission = 'maps.permission IN (?)' + if user + scope.joins(:maps).where(permission + ' OR maps.user_id = ?', visible, user.id) + else + scope.where(permission, visible) + end end end diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index e8d49548..042c9a75 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -1,7 +1,13 @@ class SynapsePolicy < ApplicationPolicy class Scope < Scope def resolve - scope.where('synapses.permission IN (?) OR synapses.user_id = ?', ["public", "commons"], user.id) + visible = ['public', 'commons'] + permission = 'synapses.permission IN (?)' + if user + scope.where(permission + ' OR synapses.user_id = ?', visible, user.id) + else + scope.where(permission, visible) + end end end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 43d4ec98..335a2ed2 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -1,7 +1,13 @@ class TopicPolicy < ApplicationPolicy class Scope < Scope def resolve - scope.where('topics.permission IN (?) OR topics.user_id = ?', ["public", "commons"], user.id) + visible = ['public', 'commons'] + permission = 'topics.permission IN (?)' + if user + scope.where(permission + ' OR topics.user_id = ?', visible, user.id) + else + scope.where(permission, visible) + end end end From 5fbf7ac34d8624e3cef12650af32252eba19062d Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 13 Mar 2016 03:16:31 +1100 Subject: [PATCH 246/305] hide metamaps_mobile in gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 43009ea4..52428f17 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ #assety stuff realtime/node_modules public/assets +public/metamaps_mobile vendor/ #secrets and config From 11e57c1b37114eee5bd9f57ca81750e31a0673ed Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 13 Mar 2016 10:36:38 +1100 Subject: [PATCH 247/305] rebase onto develop which now has API and pundit --- Gemfile | 2 + Gemfile.lock | 9 +++ Procfile | 1 + Vagrantfile | 1 - app/controllers/mappings_controller.rb | 1 + app/models/concerns/routing.rb | 10 +++ app/models/event.rb | 34 +++++++++ app/models/events/new_mapping.rb | 18 +++++ app/models/map.rb | 3 + app/models/webhook.rb | 13 ++++ app/models/webhooks/slack/base.rb | 72 +++++++++++++++++++ .../webhooks/slack/synapse_added_to_map.rb | 26 +++++++ .../webhooks/slack/topic_added_to_map.rb | 30 ++++++++ app/serializers/event_serializer.rb | 15 ++++ app/serializers/webhook_serializer.rb | 3 + app/services/webhook_service.rb | 18 +++++ config/application.rb | 2 + .../20150930233907_create_delayed_jobs.rb | 22 ++++++ db/schema.rb | 27 +++++++ 19 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 app/models/concerns/routing.rb create mode 100644 app/models/event.rb create mode 100644 app/models/events/new_mapping.rb create mode 100644 app/models/webhook.rb create mode 100644 app/models/webhooks/slack/base.rb create mode 100644 app/models/webhooks/slack/synapse_added_to_map.rb create mode 100644 app/models/webhooks/slack/topic_added_to_map.rb create mode 100644 app/serializers/event_serializer.rb create mode 100644 app/serializers/webhook_serializer.rb create mode 100644 app/services/webhook_service.rb create mode 100644 db/migrate/20150930233907_create_delayed_jobs.rb diff --git a/Gemfile b/Gemfile index b7ea08e9..7fb90aeb 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,8 @@ gem 'kaminari' # pagination gem 'uservoice-ruby' gem 'dotenv' gem 'snorlax', '~> 0.1.3' +gem 'httparty' +gem 'sequenced', '~> 2.0.0' gem 'active_model_serializers', '~> 0.8.1' gem 'paperclip' diff --git a/Gemfile.lock b/Gemfile.lock index a2a3b031..634f2031 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -97,6 +97,9 @@ GEM rails (> 3.0.0) globalid (0.3.6) activesupport (>= 4.1.0) + httparty (0.13.7) + json (~> 1.8) + multi_xml (>= 0.5.2) i18n (0.7.0) jbuilder (2.4.1) activesupport (>= 3.0.0, < 5.1) @@ -123,6 +126,7 @@ GEM mini_portile2 (2.0.0) minitest (5.8.4) multi_json (1.11.2) + multi_xml (0.5.5) nokogiri (1.6.7.2) mini_portile2 (~> 2.0.0.rc2) oauth (0.5.1) @@ -210,6 +214,9 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) + sequenced (2.0.0) + activerecord (>= 3.0) + activesupport (>= 3.0) shoulda-matchers (3.1.1) activesupport (>= 4.0.0) simplecov (0.11.2) @@ -260,6 +267,7 @@ DEPENDENCIES factory_girl_rails formtastic formula + httparty jbuilder jquery-rails jquery-ui-rails @@ -279,6 +287,7 @@ DEPENDENCIES redis rspec-rails sass-rails + sequenced (~> 2.0.0) shoulda-matchers simplecov snorlax (~> 0.1.3) diff --git a/Procfile b/Procfile index 443f2e35..802726be 100644 --- a/Procfile +++ b/Procfile @@ -1 +1,2 @@ web: bundle exec rails server -p $PORT + diff --git a/Vagrantfile b/Vagrantfile index dad6c7c7..c9ea9363 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -16,7 +16,6 @@ sudo apt-get install nodejs -y sudo apt-get install npm -y sudo apt-get install postgresql -y sudo apt-get install libpq-dev -y -sudo apt-get install redis-server -y # get imagemagick sudo apt-get install imagemagick --fix-missing diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index 649ef8f3..936ffcc2 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -22,6 +22,7 @@ class MappingsController < ApplicationController if @mapping.save render json: @mapping, status: :created + Events::NewMapping.publish!(@mapping, current_user) else render json: @mapping.errors, status: :unprocessable_entity end diff --git a/app/models/concerns/routing.rb b/app/models/concerns/routing.rb new file mode 100644 index 00000000..2f8467bf --- /dev/null +++ b/app/models/concerns/routing.rb @@ -0,0 +1,10 @@ +module Routing + extend ActiveSupport::Concern + include Rails.application.routes.url_helpers + + included do + def default_url_options + ActionMailer::Base.default_url_options + end + end +end diff --git a/app/models/event.rb b/app/models/event.rb new file mode 100644 index 00000000..b49b4856 --- /dev/null +++ b/app/models/event.rb @@ -0,0 +1,34 @@ +class Event < ActiveRecord::Base + KINDS = %w[topic_added_to_map synapse_added_to_map] + + #has_many :notifications, dependent: :destroy + belongs_to :eventable, polymorphic: true + belongs_to :map + belongs_to :user + + scope :sequenced, -> { where('sequence_id is not null').order('sequence_id asc') } + scope :chronologically, -> { order('created_at asc') } + + after_create :notify_webhooks!, if: :map + + validates_inclusion_of :kind, :in => KINDS + validates_presence_of :eventable + + acts_as_sequenced scope: :map_id, column: :sequence_id, skip: lambda {|e| e.map.nil? || e.map_id.nil? } + + #def notify!(user) + # notifications.create!(user: user) + #end + + def belongs_to?(this_user) + self.user_id == this_user.id + end + + def notify_webhooks! + #group = self.discussion.group + self.map.webhooks.each { |webhook| WebhookService.publish! webhook: webhook, event: self } + #group.webhooks.each { |webhook| WebhookService.publish! webhook: webhook, event: self } + end + handle_asynchronously :notify_webhooks! + +end diff --git a/app/models/events/new_mapping.rb b/app/models/events/new_mapping.rb new file mode 100644 index 00000000..7d6e0696 --- /dev/null +++ b/app/models/events/new_mapping.rb @@ -0,0 +1,18 @@ +class Events::NewMapping < Event + #after_create :notify_users! + + def self.publish!(mapping, user) + create!(kind: mapping.mappable_type == "Topic" ? "topic_added_to_map" : "synapse_added_to_map", + eventable: mapping, + map: mapping.map, + user: user) + end + + private + + #def notify_users! + # unless comment_vote.user == comment_vote.comment_user + # notify!(comment_vote.comment_user) + # end + #end +end diff --git a/app/models/map.rb b/app/models/map.rb index 87c8d641..acfb627d 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -7,6 +7,9 @@ class Map < ActiveRecord::Base has_many :topics, through: :topicmappings, source: :mappable, source_type: "Topic" has_many :synapses, through: :synapsemappings, source: :mappable, source_type: "Synapse" + has_many :webhooks, as: :hookable + has_many :events, -> { includes :user }, as: :eventable, dependent: :destroy + # This method associates the attribute ":image" with a file attachment has_attached_file :screenshot, :styles => { :thumb => ['188x126#', :png] diff --git a/app/models/webhook.rb b/app/models/webhook.rb new file mode 100644 index 00000000..4e20272c --- /dev/null +++ b/app/models/webhook.rb @@ -0,0 +1,13 @@ +class Webhook < ActiveRecord::Base + belongs_to :hookable, polymorphic: true + + validates :uri, presence: true + validates :hookable, presence: true + validates_inclusion_of :kind, in: %w[slack] + validates :event_types, length: { minimum: 1 } + + def headers + {} + end + +end diff --git a/app/models/webhooks/slack/base.rb b/app/models/webhooks/slack/base.rb new file mode 100644 index 00000000..98503916 --- /dev/null +++ b/app/models/webhooks/slack/base.rb @@ -0,0 +1,72 @@ +Webhooks::Slack::Base = Struct.new(:event) do + include Routing + + def username + "Metamaps Bot" + end + + def icon_url + "https://pbs.twimg.com/profile_images/539300245029392385/dJ1bwnw7.jpeg" + end + + def text + "something" + end + + def attachments + [{ + title: attachment_title, + text: attachment_text, + fields: attachment_fields, + fallback: attachment_fallback + }] + end + + alias :read_attribute_for_serialization :send + + private + + #def motion_vote_field + # { + # title: "Vote on this proposal", + # value: "#{proposal_link(eventable, "yes")} · " + + # "#{proposal_link(eventable, "abstain")} · " + + # "#{proposal_link(eventable, "no")} · " + + # "#{proposal_link(eventable, "block")}" + # } + #end + + def view_map_on_metamaps(text = nil) + "<#{map_url(eventable.map)}|#{text || eventable.map.name}>" + end + + #def view_discussion_on_loomio(params = {}) + # { value: discussion_link(I18n.t(:"webhooks.slack.view_it_on_loomio"), params) } + #end + + #def proposal_link(proposal, position = nil) + # discussion_link position || proposal.name, { proposal: proposal.key, position: position } + #end + + #def discussion_link(text = nil, params = {}) + # "<#{discussion_url(eventable.map, params)}|#{text || eventable.discussion.title}>" + #end + + def eventable + @eventable ||= event.eventable + end + + def author + @author ||= eventable.author + end + +end + +#webhooks: +# slack: +# motion_closed: "*%{name}* has closed" +# motion_closing_soon: "*%{name}* has a proposal closing in 24 hours" +# motion_outcome_created: "*%{author}* published an outcome in *%{name}*" +# motion_outcome_updated: "*%{author}* updated the outcome for *%{name}*" +# new_motion: "*%{author}* started a new proposal in *%{name}*" +# view_it_on_loomio: "View it on Loomio" diff --git a/app/models/webhooks/slack/synapse_added_to_map.rb b/app/models/webhooks/slack/synapse_added_to_map.rb new file mode 100644 index 00000000..25093127 --- /dev/null +++ b/app/models/webhooks/slack/synapse_added_to_map.rb @@ -0,0 +1,26 @@ +class Webhooks::Slack::SynapseAddedToMap < Webhooks::Slack::Base + + def text + "\"*#{eventable.synapse.topic1.name}* #{eventable.synapse.desc || '->'} *#{eventable.synapse.topic2.name}*\" was added as a connection to the map *#{view_map_on_metamaps()}*" + end + + def attachment_fallback + "" #{}"*#{eventable.name}*\n#{eventable.description}\n" + end + + def attachment_title + "" #proposal_link(eventable) + end + + def attachment_text + "" # "#{eventable.description}\n" + end + + def attachment_fields + [{ + title: "nothing", + value: "nothing" + }] #[motion_vote_field] + end + +end diff --git a/app/models/webhooks/slack/topic_added_to_map.rb b/app/models/webhooks/slack/topic_added_to_map.rb new file mode 100644 index 00000000..6b4a16c8 --- /dev/null +++ b/app/models/webhooks/slack/topic_added_to_map.rb @@ -0,0 +1,30 @@ +class Webhooks::Slack::TopicAddedToMap < Webhooks::Slack::Base + + def text + "New #{eventable.topic.metacode.name} topic *#{eventable.topic.name}* was added to the map *#{view_map_on_metamaps()}*" + end + + def icon_url + eventable.topic.metacode.icon + end + + def attachment_fallback + "" #{}"*#{eventable.name}*\n#{eventable.description}\n" + end + + def attachment_title + "" #proposal_link(eventable) + end + + def attachment_text + "" # "#{eventable.description}\n" + end + + def attachment_fields + [{ + title: "nothing", + value: "nothing" + }] #[motion_vote_field] + end + +end diff --git a/app/serializers/event_serializer.rb b/app/serializers/event_serializer.rb new file mode 100644 index 00000000..0e87cd44 --- /dev/null +++ b/app/serializers/event_serializer.rb @@ -0,0 +1,15 @@ +class EventSerializer < ActiveModel::Serializer + embed :ids, include: true + attributes :id, :sequence_id, :kind, :map_id, :created_at + + has_one :actor, serializer: NewUserSerializer, root: 'users' + has_one :map, serializer: NewMapSerializer + + def actor + object.user || object.eventable.try(:user) + end + + def map + object.eventable.try(:map) || object.eventable.map + end +end diff --git a/app/serializers/webhook_serializer.rb b/app/serializers/webhook_serializer.rb new file mode 100644 index 00000000..9adfd101 --- /dev/null +++ b/app/serializers/webhook_serializer.rb @@ -0,0 +1,3 @@ +class WebhookSerializer < ActiveModel::Serializer + attributes :text, :username #, :attachments #, :icon_url +end diff --git a/app/services/webhook_service.rb b/app/services/webhook_service.rb new file mode 100644 index 00000000..7a4361b4 --- /dev/null +++ b/app/services/webhook_service.rb @@ -0,0 +1,18 @@ +class WebhookService + + def self.publish!(webhook:, event:) + return false unless webhook.event_types.include? event.kind + HTTParty.post webhook.uri, body: payload_for(webhook, event), headers: webhook.headers + end + + private + + def self.payload_for(webhook, event) + WebhookSerializer.new(webhook_object_for(webhook, event), root: false).to_json + end + + def self.webhook_object_for(webhook, event) + "Webhooks::#{webhook.kind.classify}::#{event.kind.classify}".constantize.new(event) + end + +end diff --git a/config/application.rb b/config/application.rb index 658a4203..04526695 100644 --- a/config/application.rb +++ b/config/application.rb @@ -10,6 +10,8 @@ Dotenv.load ".env.#{ENV["RAILS_ENV"]}", '.env' module Metamaps class Application < Rails::Application + config.active_job.queue_adapter = :delayed_job + # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. diff --git a/db/migrate/20150930233907_create_delayed_jobs.rb b/db/migrate/20150930233907_create_delayed_jobs.rb new file mode 100644 index 00000000..27fdcf6c --- /dev/null +++ b/db/migrate/20150930233907_create_delayed_jobs.rb @@ -0,0 +1,22 @@ +class CreateDelayedJobs < ActiveRecord::Migration + def self.up + create_table :delayed_jobs, force: true do |table| + table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue + table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually. + table.text :handler, null: false # YAML-encoded string of the object that will do work + table.text :last_error # reason for last failure (See Note below) + table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. + table.datetime :locked_at # Set when a client is working on this object + table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) + table.string :locked_by # Who is working on this object (if locked) + table.string :queue # The name of the queue this job is in + table.timestamps null: true + end + + add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority" + end + + def self.down + drop_table :delayed_jobs + end +end diff --git a/db/schema.rb b/db/schema.rb index 334fd4ad..38570d2f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -16,6 +16,23 @@ ActiveRecord::Schema.define(version: 20160310200131) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "events", force: :cascade do |t| + t.string "kind", limit: 255 + t.datetime "created_at" + t.datetime "updated_at" + t.integer "eventable_id" + t.string "eventable_type", limit: 255 + t.integer "user_id" + t.integer "map_id" + t.integer "sequence_id" + end + + add_index "events", ["created_at"], name: "index_events_on_created_at", using: :btree + add_index "events", ["map_id", "sequence_id"], name: "index_events_on_map_id_and_sequence_id", unique: true, using: :btree + add_index "events", ["map_id"], name: "index_events_on_map_id", using: :btree + add_index "events", ["eventable_type", "eventable_id"], name: "index_events_on_eventable_type_and_eventable_id", using: :btree + add_index "events", ["sequence_id"], name: "index_events_on_sequence_id", using: :btree + create_table "in_metacode_sets", force: :cascade do |t| t.integer "metacode_id" t.integer "metacode_set_id" @@ -181,5 +198,15 @@ ActiveRecord::Schema.define(version: 20160310200131) do add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree + create_table "webhooks", force: :cascade do |t| + t.integer "hookable_id" + t.string "hookable_type" + t.string "kind", null: false + t.string "uri", null: false + t.text "event_types", default: [], array: true + end + + add_index "webhooks", ["hookable_type", "hookable_id"], name: "index_webhooks_on_hookable_type_and_hookable_id", using: :btree + add_foreign_key "tokens", "users" end From d863d1c15bd811b4599eec0f55ca8f75a982bc78 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 13 Mar 2016 10:42:55 +1100 Subject: [PATCH 248/305] remove traces of delayed_job --- config/application.rb | 1 - .../20150930233907_create_delayed_jobs.rb | 22 ------------------- 2 files changed, 23 deletions(-) delete mode 100644 db/migrate/20150930233907_create_delayed_jobs.rb diff --git a/config/application.rb b/config/application.rb index 04526695..e2030483 100644 --- a/config/application.rb +++ b/config/application.rb @@ -10,7 +10,6 @@ Dotenv.load ".env.#{ENV["RAILS_ENV"]}", '.env' module Metamaps class Application < Rails::Application - config.active_job.queue_adapter = :delayed_job # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers diff --git a/db/migrate/20150930233907_create_delayed_jobs.rb b/db/migrate/20150930233907_create_delayed_jobs.rb deleted file mode 100644 index 27fdcf6c..00000000 --- a/db/migrate/20150930233907_create_delayed_jobs.rb +++ /dev/null @@ -1,22 +0,0 @@ -class CreateDelayedJobs < ActiveRecord::Migration - def self.up - create_table :delayed_jobs, force: true do |table| - table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue - table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually. - table.text :handler, null: false # YAML-encoded string of the object that will do work - table.text :last_error # reason for last failure (See Note below) - table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. - table.datetime :locked_at # Set when a client is working on this object - table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) - table.string :locked_by # Who is working on this object (if locked) - table.string :queue # The name of the queue this job is in - table.timestamps null: true - end - - add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority" - end - - def self.down - drop_table :delayed_jobs - end -end From fe578ca3b26f123e05c46bf519e85af7e28126d7 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 13 Mar 2016 11:24:00 +1100 Subject: [PATCH 249/305] add migrations --- db/migrate/20160312234946_create_events.rb | 13 +++++++++++++ db/migrate/20160312235006_create_webhooks.rb | 10 ++++++++++ db/schema.rb | 12 ++++++------ 3 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 db/migrate/20160312234946_create_events.rb create mode 100644 db/migrate/20160312235006_create_webhooks.rb diff --git a/db/migrate/20160312234946_create_events.rb b/db/migrate/20160312234946_create_events.rb new file mode 100644 index 00000000..7ab71099 --- /dev/null +++ b/db/migrate/20160312234946_create_events.rb @@ -0,0 +1,13 @@ +class CreateEvents < ActiveRecord::Migration + def change + create_table :events do |t| + t.string :kind, limit: 255 + t.references :eventable, polymorphic: true, index: true + t.references :user, index: true + t.references :map, index: true + t.integer :sequence_id, index: true, default: nil, null: true + t.timestamps + end + add_index :events, [:map_id, :sequence_id], unique: true + end +end diff --git a/db/migrate/20160312235006_create_webhooks.rb b/db/migrate/20160312235006_create_webhooks.rb new file mode 100644 index 00000000..0d1cf899 --- /dev/null +++ b/db/migrate/20160312235006_create_webhooks.rb @@ -0,0 +1,10 @@ +class CreateWebhooks < ActiveRecord::Migration + def change + create_table :webhooks do |t| + t.references :hookable, polymorphic: true, index: true + t.string :kind, null: false + t.string :uri, null: false + t.text :event_types, array: true, default: [] + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 38570d2f..9bb034ab 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,27 +11,27 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160310200131) do +ActiveRecord::Schema.define(version: 20160312235006) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" create_table "events", force: :cascade do |t| t.string "kind", limit: 255 - t.datetime "created_at" - t.datetime "updated_at" t.integer "eventable_id" - t.string "eventable_type", limit: 255 + t.string "eventable_type" t.integer "user_id" t.integer "map_id" t.integer "sequence_id" + t.datetime "created_at" + t.datetime "updated_at" end - add_index "events", ["created_at"], name: "index_events_on_created_at", using: :btree + add_index "events", ["eventable_type", "eventable_id"], name: "index_events_on_eventable_type_and_eventable_id", using: :btree add_index "events", ["map_id", "sequence_id"], name: "index_events_on_map_id_and_sequence_id", unique: true, using: :btree add_index "events", ["map_id"], name: "index_events_on_map_id", using: :btree - add_index "events", ["eventable_type", "eventable_id"], name: "index_events_on_eventable_type_and_eventable_id", using: :btree add_index "events", ["sequence_id"], name: "index_events_on_sequence_id", using: :btree + add_index "events", ["user_id"], name: "index_events_on_user_id", using: :btree create_table "in_metacode_sets", force: :cascade do |t| t.integer "metacode_id" From 77d69dd2a363c73a3655509e7df00a25c585b475 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 13 Mar 2016 11:56:18 +1100 Subject: [PATCH 250/305] turns out we do need delayed_job --- Gemfile | 2 ++ Gemfile.lock | 7 ++++++ Procfile | 1 + .../webhooks/slack/synapse_added_to_map.rb | 2 +- .../webhooks/slack/topic_added_to_map.rb | 4 ++-- app/serializers/webhook_serializer.rb | 2 +- bin/delayed_job | 5 +++++ config/application.rb | 2 +- .../20160313003721_create_delayed_jobs.rb | 22 +++++++++++++++++++ db/schema.rb | 18 ++++++++++++++- 10 files changed, 59 insertions(+), 6 deletions(-) create mode 100755 bin/delayed_job create mode 100644 db/migrate/20160313003721_create_delayed_jobs.rb diff --git a/Gemfile b/Gemfile index 7fb90aeb..bf5997af 100644 --- a/Gemfile +++ b/Gemfile @@ -21,6 +21,8 @@ gem 'snorlax', '~> 0.1.3' gem 'httparty' gem 'sequenced', '~> 2.0.0' gem 'active_model_serializers', '~> 0.8.1' +gem 'delayed_job', '~> 4.0.2' +gem 'delayed_job_active_record', '~> 4.0.1' gem 'paperclip' gem 'aws-sdk', '< 2.0' diff --git a/Gemfile.lock b/Gemfile.lock index 634f2031..ff5c1317 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,6 +73,11 @@ GEM coffee-script-source (1.10.0) concurrent-ruby (1.0.1) debug_inspector (0.0.2) + delayed_job (4.0.6) + activesupport (>= 3.0, < 5.0) + delayed_job_active_record (4.0.3) + activerecord (>= 3.0, < 5.0) + delayed_job (>= 3.0, < 4.1) devise (3.5.6) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -262,6 +267,8 @@ DEPENDENCIES binding_of_caller cancan coffee-rails + delayed_job (~> 4.0.2) + delayed_job_active_record (~> 4.0.1) devise dotenv factory_girl_rails diff --git a/Procfile b/Procfile index 802726be..e00c3019 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,3 @@ web: bundle exec rails server -p $PORT +worker: bundle exec rake jobs:work diff --git a/app/models/webhooks/slack/synapse_added_to_map.rb b/app/models/webhooks/slack/synapse_added_to_map.rb index 25093127..3d70450b 100644 --- a/app/models/webhooks/slack/synapse_added_to_map.rb +++ b/app/models/webhooks/slack/synapse_added_to_map.rb @@ -1,7 +1,7 @@ class Webhooks::Slack::SynapseAddedToMap < Webhooks::Slack::Base def text - "\"*#{eventable.synapse.topic1.name}* #{eventable.synapse.desc || '->'} *#{eventable.synapse.topic2.name}*\" was added as a connection to the map *#{view_map_on_metamaps()}*" + "\"*#{eventable.mappable.topic1.name}* #{eventable.mappable.desc || '->'} *#{eventable.mappable.topic2.name}*\" was added as a connection to the map *#{view_map_on_metamaps()}*" end def attachment_fallback diff --git a/app/models/webhooks/slack/topic_added_to_map.rb b/app/models/webhooks/slack/topic_added_to_map.rb index 6b4a16c8..89e1194b 100644 --- a/app/models/webhooks/slack/topic_added_to_map.rb +++ b/app/models/webhooks/slack/topic_added_to_map.rb @@ -1,11 +1,11 @@ class Webhooks::Slack::TopicAddedToMap < Webhooks::Slack::Base def text - "New #{eventable.topic.metacode.name} topic *#{eventable.topic.name}* was added to the map *#{view_map_on_metamaps()}*" + "New #{eventable.mappable.metacode.name} topic *#{eventable.mappable.name}* was added to the map *#{view_map_on_metamaps()}*" end def icon_url - eventable.topic.metacode.icon + eventable.mappable.metacode.icon end def attachment_fallback diff --git a/app/serializers/webhook_serializer.rb b/app/serializers/webhook_serializer.rb index 9adfd101..ed013cae 100644 --- a/app/serializers/webhook_serializer.rb +++ b/app/serializers/webhook_serializer.rb @@ -1,3 +1,3 @@ class WebhookSerializer < ActiveModel::Serializer - attributes :text, :username #, :attachments #, :icon_url + attributes :text, :username, :icon_url #, :attachments end diff --git a/bin/delayed_job b/bin/delayed_job new file mode 100755 index 00000000..edf19598 --- /dev/null +++ b/bin/delayed_job @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby + +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) +require 'delayed/command' +Delayed::Command.new(ARGV).daemonize diff --git a/config/application.rb b/config/application.rb index e2030483..e7a47614 100644 --- a/config/application.rb +++ b/config/application.rb @@ -10,7 +10,7 @@ Dotenv.load ".env.#{ENV["RAILS_ENV"]}", '.env' module Metamaps class Application < Rails::Application - + config.active_job.queue_adapter = :delayed_job # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. diff --git a/db/migrate/20160313003721_create_delayed_jobs.rb b/db/migrate/20160313003721_create_delayed_jobs.rb new file mode 100644 index 00000000..27fdcf6c --- /dev/null +++ b/db/migrate/20160313003721_create_delayed_jobs.rb @@ -0,0 +1,22 @@ +class CreateDelayedJobs < ActiveRecord::Migration + def self.up + create_table :delayed_jobs, force: true do |table| + table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue + table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually. + table.text :handler, null: false # YAML-encoded string of the object that will do work + table.text :last_error # reason for last failure (See Note below) + table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. + table.datetime :locked_at # Set when a client is working on this object + table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) + table.string :locked_by # Who is working on this object (if locked) + table.string :queue # The name of the queue this job is in + table.timestamps null: true + end + + add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority" + end + + def self.down + drop_table :delayed_jobs + end +end diff --git a/db/schema.rb b/db/schema.rb index 9bb034ab..d0381055 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,11 +11,27 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160312235006) do +ActiveRecord::Schema.define(version: 20160313003721) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "delayed_jobs", force: :cascade do |t| + t.integer "priority", default: 0, null: false + t.integer "attempts", default: 0, null: false + t.text "handler", null: false + t.text "last_error" + t.datetime "run_at" + t.datetime "locked_at" + t.datetime "failed_at" + t.string "locked_by" + t.string "queue" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree + create_table "events", force: :cascade do |t| t.string "kind", limit: 255 t.integer "eventable_id" From 72b2e8f8f220ebfb104c22a8f4ffa61760904107 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 13 Mar 2016 11:58:09 +1100 Subject: [PATCH 251/305] doesn't look good for now, take it out, add it later --- app/models/webhooks/slack/topic_added_to_map.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/models/webhooks/slack/topic_added_to_map.rb b/app/models/webhooks/slack/topic_added_to_map.rb index 89e1194b..3574a464 100644 --- a/app/models/webhooks/slack/topic_added_to_map.rb +++ b/app/models/webhooks/slack/topic_added_to_map.rb @@ -3,10 +3,7 @@ class Webhooks::Slack::TopicAddedToMap < Webhooks::Slack::Base def text "New #{eventable.mappable.metacode.name} topic *#{eventable.mappable.name}* was added to the map *#{view_map_on_metamaps()}*" end - - def icon_url - eventable.mappable.metacode.icon - end + # todo: it would be sweet if it sends it with the metacode as the icon_url def attachment_fallback "" #{}"*#{eventable.name}*\n#{eventable.description}\n" From 74cf9ba7177db6d11100f6f8fd1f2306fc90d789 Mon Sep 17 00:00:00 2001 From: Harlan T Wood Date: Sat, 12 Mar 2016 19:40:56 -0800 Subject: [PATCH 252/305] Run tests on Travis CI; show build status badge --- .travis.yml | 9 +++++++++ README.md | 8 ++++---- 2 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..58bf9d25 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +sudo: false +language: ruby +rvm: + - 2.1.3 +before_script: + - export RAILS_ENV=test + - cp .example-env .env + - bundle exec rake db:create + - bundle exec rake db:schema:load diff --git a/README.md b/README.md index 293265ac..cf871b82 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Metamaps ======= [![Join the chat at https://gitter.im/metamaps/metamaps_gen002](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/metamaps/metamaps_gen002?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Build Status](https://jenkins.devinhoward.ca/job/metamaps_gen002.develop/badge/icon)](https://jenkins.devinhoward.ca/job/metamaps_gen002.develop/) +[![Build Status](https://travis-ci.org/metamaps/metamaps_gen002.svg)](https://travis-ci.org/metamaps/metamaps_gen002) -Welcome to the Metamaps GitHub repo. +Welcome to the Metamaps GitHub repo. ## About @@ -12,7 +12,7 @@ Metamaps is a free and AGPL open source technology for changemakers, innovators, You can find a version of this software running at [metamaps.cc][site-beta], where the technology is being tested in a private beta. -Metamaps is created and maintained by a distributed, nomadic community comprised of technologists, artists and storytellers. You can get in touch with us at team@metamaps.cc or @metamapps on twitter. +Metamaps is created and maintained by a distributed, nomadic community comprised of technologists, artists and storytellers. You can get in touch with us at team@metamaps.cc or @metamapps on twitter. To get connected with the community interested in Metamaps, join our [Google+ community][community]. @@ -52,7 +52,7 @@ We haven't figured out Vagrant for Windows yet, but we have a set of manual inst ## Contributing -Cloning this repository directly is primarily for those wishing to contribute to our codebase. Check out our [contributing instructions][contributing] to get involved. +Cloning this repository directly is primarily for those wishing to contribute to our codebase. Check out our [contributing instructions][contributing] to get involved. ## Community From 3fbb3d1dc9b9d80ba2b7d843d7e89e0f432b1a71 Mon Sep 17 00:00:00 2001 From: Harlan T Wood Date: Sun, 13 Mar 2016 00:20:18 -0800 Subject: [PATCH 253/305] more token entropy --- app/models/token.rb | 15 +++++++++++++-- spec/models/token_spec.rb | 7 ++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/models/token.rb b/app/models/token.rb index 15bdca79..714730fc 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -1,11 +1,22 @@ class Token < ActiveRecord::Base belongs_to :user - before_create :generate_token + before_create :assign_token + + CHARS = 32 private + def assign_token + self.token = generate_token + end + def generate_token - self.token = SecureRandom.uuid.gsub(/\-/,'') + loop do + candidate = SecureRandom.base64(CHARS).gsub(/\W/, '') + if candidate.size >= CHARS + return candidate[0...CHARS] + end + end end end diff --git a/spec/models/token_spec.rb b/spec/models/token_spec.rb index 18bba17d..9fedeb2a 100644 --- a/spec/models/token_spec.rb +++ b/spec/models/token_spec.rb @@ -1,5 +1,10 @@ require 'rails_helper' RSpec.describe Token, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + context "#generate_token" do + subject (:token) { Token.new } + it "should generate an alphanumeric token of 32 characters" do + expect(token.send(:generate_token)).to match /[a-zA-Z0-9]{32}/ + end + end end From f3eb55897156bd4e2e41460d66a1edabe60c29aa Mon Sep 17 00:00:00 2001 From: Harlan T Wood Date: Sun, 13 Mar 2016 13:28:07 -0700 Subject: [PATCH 254/305] fix test regex --- spec/models/token_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/token_spec.rb b/spec/models/token_spec.rb index 9fedeb2a..ddb8d696 100644 --- a/spec/models/token_spec.rb +++ b/spec/models/token_spec.rb @@ -4,7 +4,7 @@ RSpec.describe Token, type: :model do context "#generate_token" do subject (:token) { Token.new } it "should generate an alphanumeric token of 32 characters" do - expect(token.send(:generate_token)).to match /[a-zA-Z0-9]{32}/ + expect(token.send(:generate_token)).to match /^[a-zA-Z0-9]{32}$/ end end end From 6f5258cbb7bd5ed145dba95d2d1c594f41fbbe85 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 14 Mar 2016 08:19:26 +1100 Subject: [PATCH 255/305] needed a policy for tokens --- app/policies/token_policy.rb | 24 ++++++++++++++++++++++++ app/serializers/token_serializer.rb | 2 -- 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 app/policies/token_policy.rb diff --git a/app/policies/token_policy.rb b/app/policies/token_policy.rb new file mode 100644 index 00000000..393d2441 --- /dev/null +++ b/app/policies/token_policy.rb @@ -0,0 +1,24 @@ +class TokenPolicy < ApplicationPolicy + class Scope < Scope + def resolve + if user + scope.where('tokens.user_id = ?', user.id) + else + where(:id => nil).where("id IS NOT ?", nil) # to just return none + end + end + end + + def create? + user.present? + end + + def my_tokens? + user.present? + end + + def destroy? + user.present? && record.user == user + end + +end diff --git a/app/serializers/token_serializer.rb b/app/serializers/token_serializer.rb index 8eed535a..7abcc3df 100644 --- a/app/serializers/token_serializer.rb +++ b/app/serializers/token_serializer.rb @@ -1,9 +1,7 @@ class TokenSerializer < ActiveModel::Serializer - embed :ids, include: true attributes :id, :token, :description, - :user_id, :created_at, :updated_at From 6715ba7e7f5529192bc46d16d79baa201ffd250b Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 14 Mar 2016 11:03:11 +1100 Subject: [PATCH 256/305] not a function --- Gemfile.lock | 1 - app/helpers/topics_helper.rb | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index bd124e55..ff5c1317 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -71,7 +71,6 @@ GEM coffee-script-source execjs coffee-script-source (1.10.0) - columnize (0.9.0) concurrent-ruby (1.0.1) debug_inspector (0.0.2) delayed_job (4.0.6) diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index 362a5f46..482a663c 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -10,7 +10,7 @@ module TopicsHelper topic['value'] = t.name topic['description'] = t.desc.truncate(70) # make this return matched results topic['type'] = t.metacode.name - topic['typeImageURL'] = t.metacode.asset_path_icon + topic['typeImageURL'] = t.metacode.icon topic['permission'] = t.permission topic['mapCount'] = t.maps.count topic['synapseCount'] = t.synapses.count From 579c36ec7518d442b66b1e8e5c202131176d9cbe Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 14 Mar 2016 11:10:18 +1100 Subject: [PATCH 257/305] ensure the search box opens --- app/views/maps/activemaps.html.erb | 2 +- app/views/maps/featuredmaps.html.erb | 2 +- app/views/maps/mymaps.html.erb | 2 +- app/views/maps/usermaps.html.erb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/maps/activemaps.html.erb b/app/views/maps/activemaps.html.erb index 90156321..cfcbce27 100644 --- a/app/views/maps/activemaps.html.erb +++ b/app/views/maps/activemaps.html.erb @@ -10,6 +10,6 @@ <% content_for :title, "Explore Active Maps | Metamaps" %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.isOpen = true; + Metamaps.GlobalUI.Search.open(); Metamaps.GlobalUI.Search.lock(); diff --git a/app/views/maps/featuredmaps.html.erb b/app/views/maps/featuredmaps.html.erb index 048d23a4..2c438b49 100644 --- a/app/views/maps/featuredmaps.html.erb +++ b/app/views/maps/featuredmaps.html.erb @@ -10,6 +10,6 @@ <% content_for :title, "Explore Featured Maps | Metamaps" %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.isOpen = true; + Metamaps.GlobalUI.Search.open(); Metamaps.GlobalUI.Search.lock(); diff --git a/app/views/maps/mymaps.html.erb b/app/views/maps/mymaps.html.erb index 76a96ec1..60c69f68 100644 --- a/app/views/maps/mymaps.html.erb +++ b/app/views/maps/mymaps.html.erb @@ -10,6 +10,6 @@ <% content_for :title, "Explore My Maps | Metamaps" %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.isOpen = true; + Metamaps.GlobalUI.Search.open(); Metamaps.GlobalUI.Search.lock(); diff --git a/app/views/maps/usermaps.html.erb b/app/views/maps/usermaps.html.erb index 4107fbb7..c0855329 100644 --- a/app/views/maps/usermaps.html.erb +++ b/app/views/maps/usermaps.html.erb @@ -13,6 +13,6 @@ <% content_for :title, @user.name + " | Metamaps" %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.isOpen = true; + Metamaps.GlobalUI.Search.open(); Metamaps.GlobalUI.Search.lock(); From 88c070cbbd0fcc52dd338fbba93147775269af1c Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 14 Mar 2016 10:55:26 +0800 Subject: [PATCH 258/305] no cancan --- Gemfile | 1 - Gemfile.lock | 5 ----- 2 files changed, 6 deletions(-) diff --git a/Gemfile b/Gemfile index bf5997af..497172e4 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,6 @@ gem 'devise' gem 'redis' gem 'pg' gem 'pundit' -gem 'cancan' gem 'pundit_extra' gem 'formula' gem 'formtastic' diff --git a/Gemfile.lock b/Gemfile.lock index ff5c1317..86fea5e0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -57,7 +57,6 @@ GEM debug_inspector (>= 0.0.1) builder (3.2.2) byebug (8.2.2) - cancan (1.6.10) cancancan (1.10.1) climate_control (0.0.3) activesupport (>= 3.0) @@ -265,7 +264,6 @@ DEPENDENCIES best_in_place better_errors binding_of_caller - cancan coffee-rails delayed_job (~> 4.0.2) delayed_job_active_record (~> 4.0.1) @@ -301,6 +299,3 @@ DEPENDENCIES tunemygc uglifier uservoice-ruby - -BUNDLED WITH - 1.11.2 From dbb8052a17650744e2ef75ef1367ac7d3be51f2d Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 14 Mar 2016 11:00:54 +0800 Subject: [PATCH 259/305] trifecta of policy tests --- spec/policies/map_policy_spec.rb | 31 +++++----- spec/policies/synapse_policy.rb | 92 ++++++++++++++++++++++++++++++ spec/policies/topic_policy_spec.rb | 92 ++++++++++++++++++++++++++++++ 3 files changed, 197 insertions(+), 18 deletions(-) create mode 100644 spec/policies/synapse_policy.rb create mode 100644 spec/policies/topic_policy_spec.rb diff --git a/spec/policies/map_policy_spec.rb b/spec/policies/map_policy_spec.rb index b160fead..7dd33707 100644 --- a/spec/policies/map_policy_spec.rb +++ b/spec/policies/map_policy_spec.rb @@ -7,12 +7,12 @@ RSpec.describe MapPolicy, type: :policy do context 'commons' do let(:map) { create(:map, permission: :commons) } permissions :show? do - it 'can view' do + it 'permits access' do expect(subject).to permit(nil, map) end end permissions :create?, :update?, :destroy? do - it 'can not modify' do + it 'denies access' do expect(subject).to_not permit(nil, map) end end @@ -21,7 +21,7 @@ RSpec.describe MapPolicy, type: :policy do context 'private' do let(:map) { create(:map, permission: :private) } permissions :show?, :create?, :update?, :destroy? do - it 'can not view or modify' do + it 'permits access' do expect(subject).to_not permit(nil, map) end end @@ -39,15 +39,15 @@ RSpec.describe MapPolicy, type: :policy do let(:owner) { create(:user) } let(:map) { create(:map, permission: :commons, user: owner) } permissions :show?, :create?, :update? do - it 'can view and modify' do + it 'permits access' do expect(subject).to permit(user, map) end end permissions :destroy? do - it 'can not destroy' do + it 'denies access' do expect(subject).to_not permit(user, map) end - it 'owner can destroy' do + it 'permits access to owner' do expect(subject).to permit(owner, map) end end @@ -56,21 +56,16 @@ RSpec.describe MapPolicy, type: :policy do context 'public' do let(:owner) { create(:user) } let(:map) { create(:map, permission: :public, user: owner) } - permissions :show? do - it 'can view' do - expect(subject).to permit(user, map) - end - end - permissions :create? do - it 'can create' do + permissions :show?, :create? do + it 'permits access' do expect(subject).to permit(user, map) end end permissions :update?, :destroy? do - it 'can not update/destroy' do + it 'denies access' do expect(subject).to_not permit(user, map) end - it 'owner can update/destroy' do + it 'permits access to owner' do expect(subject).to permit(owner, map) end end @@ -80,15 +75,15 @@ RSpec.describe MapPolicy, type: :policy do let(:owner) { create(:user) } let(:map) { create(:map, permission: :private, user: owner) } permissions :create? do - it 'can create' do + it 'permits access' do expect(subject).to permit(user, map) end end permissions :show?, :update?, :destroy? do - it 'can not view or modify' do + it 'denies access' do expect(subject).to_not permit(user, map) end - it 'owner can view and modify' do + it 'permits access to owner' do expect(subject).to permit(owner, map) end end diff --git a/spec/policies/synapse_policy.rb b/spec/policies/synapse_policy.rb new file mode 100644 index 00000000..4c725e37 --- /dev/null +++ b/spec/policies/synapse_policy.rb @@ -0,0 +1,92 @@ +require 'rails_helper' + +RSpec.describe SynapsePolicy, type: :policy do + subject { described_class } + + context 'unauthenticated' do + context 'commons' do + let(:synapse) { create(:synapse, permission: :commons) } + permissions :show? do + it 'permits access' do + expect(subject).to permit(nil, synapse) + end + end + permissions :create?, :update?, :destroy? do + it 'denies access' do + expect(subject).to_not permit(nil, synapse) + end + end + end + + context 'private' do + let(:synapse) { create(:synapse, permission: :private) } + permissions :show?, :create?, :update?, :destroy? do + it 'denies access' do + expect(subject).to_not permit(nil, synapse) + end + end + end + end + + # + # Now begin the logged-in tests + # + + context 'logged in' do + let(:user) { create(:user) } + + context 'commons' do + let(:owner) { create(:user) } + let(:synapse) { create(:synapse, permission: :commons, user: owner) } + permissions :show?, :create?, :update? do + it 'permits access' do + expect(subject).to permit(user, synapse) + end + end + permissions :destroy? do + it 'denies access' do + expect(subject).to_not permit(user, synapse) + end + it 'permits access to owner' do + expect(subject).to permit(owner, synapse) + end + end + end + + context 'public' do + let(:owner) { create(:user) } + let(:synapse) { create(:synapse, permission: :public, user: owner) } + permissions :show?, :create? do + it 'permits access' do + expect(subject).to permit(user, synapse) + end + end + permissions :update?, :destroy? do + it 'denies access' do + expect(subject).to_not permit(user, synapse) + end + it 'permits access to owner' do + expect(subject).to permit(owner, synapse) + end + end + end + + context 'private' do + let(:owner) { create(:user) } + let(:synapse) { create(:synapse, permission: :private, user: owner) } + permissions :create? do + it 'permits access' do + expect(subject).to permit(user, synapse) + end + end + permissions :show?, :update?, :destroy? do + it 'denies access' do + expect(subject).to_not permit(user, synapse) + end + it 'permits access to owner' do + expect(subject).to permit(owner, synapse) + end + end + end + end +end diff --git a/spec/policies/topic_policy_spec.rb b/spec/policies/topic_policy_spec.rb new file mode 100644 index 00000000..7078496c --- /dev/null +++ b/spec/policies/topic_policy_spec.rb @@ -0,0 +1,92 @@ +require 'rails_helper' + +RSpec.describe TopicPolicy, type: :policy do + subject { described_class } + + context 'unauthenticated' do + context 'commons' do + let(:topic) { create(:topic, permission: :commons) } + permissions :show? do + it 'permits access' do + expect(subject).to permit(nil, topic) + end + end + permissions :create?, :update?, :destroy? do + it 'denies access' do + expect(subject).to_not permit(nil, topic) + end + end + end + + context 'private' do + let(:topic) { create(:topic, permission: :private) } + permissions :show?, :create?, :update?, :destroy? do + it 'denies access' do + expect(subject).to_not permit(nil, topic) + end + end + end + end + + # + # Now begin the logged-in tests + # + + context 'logged in' do + let(:user) { create(:user) } + + context 'commons' do + let(:owner) { create(:user) } + let(:topic) { create(:topic, permission: :commons, user: owner) } + permissions :show?, :create?, :update? do + it 'permits access' do + expect(subject).to permit(user, topic) + end + end + permissions :destroy? do + it 'denies access' do + expect(subject).to_not permit(user, topic) + end + it 'permits access to owner' do + expect(subject).to permit(owner, topic) + end + end + end + + context 'public' do + let(:owner) { create(:user) } + let(:topic) { create(:topic, permission: :public, user: owner) } + permissions :show?, :create? do + it 'permits access' do + expect(subject).to permit(user, topic) + end + end + permissions :update?, :destroy? do + it 'denies access' do + expect(subject).to_not permit(user, topic) + end + it 'permits access to owner' do + expect(subject).to permit(owner, topic) + end + end + end + + context 'private' do + let(:owner) { create(:user) } + let(:topic) { create(:topic, permission: :private, user: owner) } + permissions :create? do + it 'permits access' do + expect(subject).to permit(user, topic) + end + end + permissions :show?, :update?, :destroy? do + it 'denies access' do + expect(subject).to_not permit(user, topic) + end + it 'permits access to owner' do + expect(subject).to permit(owner, topic) + end + end + end + end +end From c5009952f379aef3220a858d311e5bbfa03615ab Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 14 Mar 2016 11:03:30 +0800 Subject: [PATCH 260/305] remove permissions tests --- spec/models/map_spec.rb | 29 ----------------------------- spec/models/synapse_spec.rb | 29 ----------------------------- spec/models/topic_spec.rb | 29 ----------------------------- 3 files changed, 87 deletions(-) diff --git a/spec/models/map_spec.rb b/spec/models/map_spec.rb index d1486192..27227188 100644 --- a/spec/models/map_spec.rb +++ b/spec/models/map_spec.rb @@ -5,34 +5,5 @@ RSpec.describe Map, type: :model do it { is_expected.to validate_presence_of :name } it { is_expected.to validate_presence_of :permission } it { is_expected.to validate_inclusion_of(:permission).in_array Perm::ISSIONS.map(&:to_s) } - - context 'permissions' do - let(:owner) { create :user } - let(:other_user) { create :user } - let(:map) { create :map, user: owner, permission: :commons } - let(:private_map) { create :map, user: owner, permission: :private } - let(:public_map) { create :map, user: owner, permission: :public } - - it 'prevents deletion by non-owner' do - expect(map.authorize_to_delete(other_user)).to eq false - expect(map.authorize_to_delete(owner)).to eq map - end - - it 'prevents visibility if private' do - expect(map.authorize_to_show(other_user)).to eq map - expect(map.authorize_to_show(owner)).to eq map - expect(private_map.authorize_to_show(owner)).to eq private_map - expect(private_map.authorize_to_show(other_user)).to eq false - end - - it 'only allows editing if commons or owned' do - expect(map.authorize_to_edit(other_user)).to eq map - expect(map.authorize_to_edit(owner)).to eq map - expect(private_map.authorize_to_edit(other_user)).to eq false - expect(private_map.authorize_to_edit(owner)).to eq private_map - expect(public_map.authorize_to_edit(other_user)).to eq false - expect(public_map.authorize_to_edit(owner)).to eq public_map - end - end end diff --git a/spec/models/synapse_spec.rb b/spec/models/synapse_spec.rb index dcf85358..6ba5ff22 100644 --- a/spec/models/synapse_spec.rb +++ b/spec/models/synapse_spec.rb @@ -10,33 +10,4 @@ RSpec.describe Synapse, type: :model do it { is_expected.to validate_inclusion_of(:permission).in_array Perm::ISSIONS.map(&:to_s) } it { is_expected.to validate_inclusion_of(:category).in_array ['from-to', 'both'] } it { is_expected.to validate_length_of(:desc).is_at_least(0) } # TODO don't allow nil - - context 'permissions' do - let(:owner) { create :user } - let(:other_user) { create :user } - let(:synapse) { create :synapse, user: owner, permission: :commons } - let(:private_synapse) { create :synapse, user: owner, permission: :private } - let(:public_synapse) { create :synapse, user: owner, permission: :public } - - it 'prevents deletion by non-owner' do - expect(synapse.authorize_to_delete(other_user)).to eq false - expect(synapse.authorize_to_delete(owner)).to eq synapse - end - - it 'prevents visibility if private' do - expect(synapse.authorize_to_show(other_user)).to eq synapse - expect(synapse.authorize_to_show(owner)).to eq synapse - expect(private_synapse.authorize_to_show(owner)).to eq private_synapse - expect(private_synapse.authorize_to_show(other_user)).to eq false - end - - it 'only allows editing if commons or owned' do - expect(synapse.authorize_to_edit(other_user)).to eq synapse - expect(synapse.authorize_to_edit(owner)).to eq synapse - expect(private_synapse.authorize_to_edit(other_user)).to eq false - expect(private_synapse.authorize_to_edit(owner)).to eq private_synapse - expect(public_synapse.authorize_to_edit(other_user)).to eq false - expect(public_synapse.authorize_to_edit(owner)).to eq public_synapse - end - end end diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index b499daac..ef8f0b7a 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -49,33 +49,4 @@ RSpec.describe Topic, type: :model do end end end - - context 'permssions' do - let(:owner) { create :user } - let(:other_user) { create :user } - let(:topic) { create :topic, user: owner, permission: :commons } - let(:private_topic) { create :topic, user: owner, permission: :private } - let(:public_topic) { create :topic, user: owner, permission: :public } - - it 'prevents deletion by non-owner' do - expect(topic.authorize_to_delete(other_user)).to eq false - expect(topic.authorize_to_delete(owner)).to eq topic - end - - it 'prevents visibility if private' do - expect(topic.authorize_to_show(other_user)).to eq topic - expect(topic.authorize_to_show(owner)).to eq topic - expect(private_topic.authorize_to_show(owner)).to eq private_topic - expect(private_topic.authorize_to_show(other_user)).to eq false - end - - it 'only allows editing if commons or owned' do - expect(topic.authorize_to_edit(other_user)).to eq topic - expect(topic.authorize_to_edit(owner)).to eq topic - expect(private_topic.authorize_to_edit(other_user)).to eq false - expect(private_topic.authorize_to_edit(owner)).to eq private_topic - expect(public_topic.authorize_to_edit(other_user)).to eq false - expect(public_topic.authorize_to_edit(owner)).to eq public_topic - end - end end From 3823c708fd0f65edba3b9f78801d676a3e3c4258 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 14 Mar 2016 11:09:27 +0800 Subject: [PATCH 261/305] update mapping policy --- app/policies/mapping_policy.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index ed93bc66..40b71f61 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -16,20 +16,28 @@ class MappingPolicy < ApplicationPolicy end def show? - map = Pundit.policy(user, record.map) - mappable = Pundit.policy(user, record.mappable) - map.show? && mappable.show? + map_policy.show? && mappable_policy.show? end def create? - Pundit.policy(user, record.map).update? + map_policy.update? end def update? - Pundit.policy(user, record.map).update? + record.mappable_type == 'Topic' && map_policy.update? end def destroy? - record.user == user || admin_override + map_policy.update? || admin_override + end + + # Helpers + + def map_policy + @map_policy ||= Pundit.policy(user, record.map) + end + + def mappable_policy + @mappable_policy ||= Pundit.policy(user, record.mappable) end end From f7201e048e68b80ba7fc0fadd8e2b68406f43b45 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 14 Mar 2016 11:15:10 +0800 Subject: [PATCH 262/305] pending mapping policy --- spec/policies/mapping_policy_spec.rb | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 spec/policies/mapping_policy_spec.rb diff --git a/spec/policies/mapping_policy_spec.rb b/spec/policies/mapping_policy_spec.rb new file mode 100644 index 00000000..291a9c17 --- /dev/null +++ b/spec/policies/mapping_policy_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +RSpec.describe MappingPolicy, type: :policy do + subject { described_class } + + pending 'Implement some mapping tests!' +end From 7716462c8fec516a5fcf79f284fe467ae747cf98 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 14 Mar 2016 11:40:23 +0800 Subject: [PATCH 263/305] fix topics controller test --- spec/controllers/topics_controller_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index 2dd999a1..4cce6851 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -95,7 +95,7 @@ RSpec.describe TopicsController, type: :controller do end it 'return 204 NO CONTENT' do - delete :destroy, { id: topic.to_param, format: :json } + delete :destroy, { id: owned_topic.to_param, format: :json } expect(response.status).to eq 204 end end From 32326ff4afc178e1a71ebf75f3757989cb977fb8 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 14 Mar 2016 11:46:45 +0800 Subject: [PATCH 264/305] update map/topic delete action test --- spec/controllers/maps_controller_spec.rb | 6 ++++-- spec/controllers/topics_controller_spec.rb | 5 +---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/spec/controllers/maps_controller_spec.rb b/spec/controllers/maps_controller_spec.rb index 67950d75..35c3ddcc 100644 --- a/spec/controllers/maps_controller_spec.rb +++ b/spec/controllers/maps_controller_spec.rb @@ -89,7 +89,8 @@ RSpec.describe MapsController, type: :controller do expect do delete :destroy, { id: unowned_map.to_param, format: :json } end.to change(Map, :count).by(0) - expect(response.body).to eq("unauthorized") + expect(response.body).to eq '' + expect(response.status).to eq 403 end it 'deletes owned map' do @@ -97,7 +98,8 @@ RSpec.describe MapsController, type: :controller do expect do delete :destroy, { id: owned_map.to_param, format: :json } end.to change(Map, :count).by(-1) - expect(response.body).to eq("success") + expect(response.body).to eq '' + expect(response.status).to eq 204 end end end diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index 4cce6851..2fc99b22 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -92,10 +92,7 @@ RSpec.describe TopicsController, type: :controller do expect do delete :destroy, { id: owned_topic.to_param, format: :json } end.to change(Topic, :count).by(-1) - end - - it 'return 204 NO CONTENT' do - delete :destroy, { id: owned_topic.to_param, format: :json } + expect(response.body).to eq '' expect(response.status).to eq 204 end end From d11f3923dd3d047a67b0e2a15e29540b707add30 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 14 Mar 2016 14:34:36 +0800 Subject: [PATCH 265/305] remove unused has_viewable_synapses function --- app/models/topic.rb | 11 ---------- spec/models/topic_spec.rb | 42 --------------------------------------- 2 files changed, 53 deletions(-) diff --git a/app/models/topic.rb b/app/models/topic.rb index 0f312823..aef72b74 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -83,15 +83,4 @@ class Topic < ActiveRecord::Base def mk_permission Perm.short(permission) end - - # has no viewable synapses helper function - def has_viewable_synapses(current) - result = false - synapses.each do |synapse| - if synapse.authorize_to_show(current) - result = true - end - end - result - end end diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index ef8f0b7a..dbaac86d 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -7,46 +7,4 @@ RSpec.describe Topic, type: :model do it { is_expected.to have_many :mappings } it { is_expected.to validate_presence_of :permission } it { is_expected.to validate_inclusion_of(:permission).in_array Perm::ISSIONS.map(&:to_s) } - - context 'has_viewable_synapses function' do - let (:user) { create(:user) } - let (:other_user) { create(:user) } - - context 'topic with no synapses' do - let (:topic) { create(:topic) } - - it 'returns false' do - expect(topic.has_viewable_synapses(user)).to eq false - end - end - - context 'topic with one unpermitted synapse' do - let (:synapse) { create(:synapse, permission: :private, user: other_user) } - let (:topic) { create(:topic, synapses1: [synapse]) } - - it 'returns false' do - expect(topic.has_viewable_synapses(user)).to eq false - end - end - - context 'topic with one permitted synapse' do - let (:synapse) { create(:synapse, permission: :private, user: user) } - let(:topic) { create(:topic, synapses1: [synapse]) } - - it 'returns true' do - expect(topic.has_viewable_synapses(user)).to eq true - end - end - - context 'topic with one unpermitted, one permitted synapse' do - let (:synapse1) { create(:synapse, permission: :private, user: other_user) } - let (:synapse2) { create(:synapse, permission: :private, user: user) } - let (:topic) { create(:topic, synapses1: [synapse1, synapse2]) } - - it 'returns true' do - expect(topic.synapses.count).to eq 2 - expect(topic.has_viewable_synapses(user)).to eq true - end - end - end end From a05c7dc5fe497ae1f5df2ed860bfdb3ad686b6ea Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 14 Mar 2016 14:37:01 +0800 Subject: [PATCH 266/305] avoid pundit error if no map specified with a mapping --- app/policies/mapping_policy.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index 40b71f61..5826ccd4 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -20,7 +20,7 @@ class MappingPolicy < ApplicationPolicy end def create? - map_policy.update? + record.map.present? && map_policy.update? end def update? From f24def8be6e7a6fa283cbb373756560652b0b542 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 14 Mar 2016 21:36:16 +1100 Subject: [PATCH 267/305] fix up javascript errors --- app/assets/javascripts/src/Metamaps.JIT.js.erb | 2 +- app/assets/javascripts/src/Metamaps.js.erb | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.JIT.js.erb b/app/assets/javascripts/src/Metamaps.JIT.js.erb index 283359aa..273850cb 100644 --- a/app/assets/javascripts/src/Metamaps.JIT.js.erb +++ b/app/assets/javascripts/src/Metamaps.JIT.js.erb @@ -108,7 +108,7 @@ Metamaps.JIT = { _.each(results[1], function (synapse) { mapping = synapse.getMapping(); Metamaps.Synapses.remove(synapse); - Metamaps.Mappings.remove(mapping); + if (Metamaps.Mappings) Metamaps.Mappings.remove(mapping); }); if (self.vizData.length == 0) { diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index ff6d57e6..d93b39ef 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -4023,11 +4023,11 @@ Metamaps.Topic = { nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), "end"); } if (Metamaps.Create.newTopic.addSynapse && permitCreateSynapseAfter) { - Metamaps.Create.newSynapse.topic1id = tempNode.getData('topic').id; + Metamaps.Create.newSynapse.topic1id = Metamaps.tempNode.getData('topic').id; // position the form - midpoint.x = tempNode.pos.getc().x + (nodeOnViz.pos.getc().x - tempNode.pos.getc().x) / 2; - midpoint.y = tempNode.pos.getc().y + (nodeOnViz.pos.getc().y - tempNode.pos.getc().y) / 2; + midpoint.x = Metamaps.tempNode.pos.getc().x + (nodeOnViz.pos.getc().x - Metamaps.tempNode.pos.getc().x) / 2; + midpoint.y = Metamaps.tempNode.pos.getc().y + (nodeOnViz.pos.getc().y - Metamaps.tempNode.pos.getc().y) / 2; pixelPos = Metamaps.Util.coordsToPixels(midpoint); $('#new_synapse').css('left', pixelPos.x + "px"); $('#new_synapse').css('top', pixelPos.y + "px"); @@ -4037,9 +4037,9 @@ Metamaps.Topic = { modes: ["node-property:dim"], duration: 500, onComplete: function () { - tempNode = null; - tempNode2 = null; - tempInit = false; + Metamaps.tempNode = null; + Metamaps.tempNode2 = null; + Metamaps.tempInit = false; } }); } else { From 8bd032472d48f682d9d9d5974eede47f93f7f8d6 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 14 Mar 2016 21:36:34 +1100 Subject: [PATCH 268/305] topic related things weren't working at all --- app/controllers/topics_controller.rb | 24 +++++++++++++----------- app/models/topic.rb | 17 +++++++++++------ 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 1b1e9b3c..9911a7fe 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -24,9 +24,10 @@ class TopicsController < ApplicationController respond_to do |format| format.html { - @alltopics = ([@topic] + policy_scope(Topic.relatives(@topic.id))) - @allsynapses = policy_scope(Synapse.for_topic(@topic.id)) - + @alltopics = [@topic].concat(policy_scope(Topic.relatives1(@topic.id)).to_a).concat(policy_scope(Topic.relatives2(@topic.id)).to_a) + @allsynapses = policy_scope(Synapse.for_topic(@topic.id)).to_a +puts @alltopics.length +puts @allsynapses.length @allcreators = @alltopics.map(&:user).uniq @allcreators += @allsynapses.map(&:user).uniq @@ -41,8 +42,8 @@ class TopicsController < ApplicationController @topic = Topic.find(params[:id]) authorize @topic - @alltopics = [@topic] + policy_scope(@topic.relatives) - @allsynapses = policy_scope(@topic.synapses) + @alltopics = [@topic].concat(policy_scope(Topic.relatives1(@topic.id)).to_a).concat(policy_scope(Topic.relatives2(@topic.id)).to_a) + @allsynapses = policy_scope(Synapse.for_topic(@topic.id)) @allcreators = @alltopics.map(&:user).uniq @allcreators += @allsynapses.map(&:user).uniq @@ -65,8 +66,8 @@ class TopicsController < ApplicationController topicsAlreadyHas = params[:network] ? params[:network].split(',').map(&:to_i) : [] - @alltopics = policy_scope(@topic.relatives).to_a.uniq - @alltopics.delete_if! do |topic| + @alltopics = policy_scope(Topic.relatives1(@topic.id)).to_a.concat(policy_scope(Topic.relatives2(@topic.id)).to_a).uniq + @alltopics.delete_if do |topic| topicsAlreadyHas.index(topic.id) != nil end @@ -87,14 +88,15 @@ class TopicsController < ApplicationController topicsAlreadyHas = params[:network] ? params[:network].split(',').map(&:to_i) : [] - alltopics = policy_scope(@topic.relatives).to_a.uniq.delete_if do |topic| + alltopics = policy_scope(Topic.relatives1(@topic.id)).to_a.concat(policy_scope(Topic.relatives2(@topic.id)).to_a).uniq + alltopics.delete_if do |topic| topicsAlreadyHas.index(topic.id.to_s) != nil end #find synapses between topics in alltopics array - allsynapses = policy_scope(@topic.synapses) - synapse_ids = (allsynapses.map(&:topic1_id) + allsynapses.map(&:topic2_id)).uniq - allsynapses.delete_if! do |synapse| + allsynapses = policy_scope(Synapse.for_topic(@topic.id)).to_a + synapse_ids = (allsynapses.map(&:node1_id) + allsynapses.map(&:node2_id)).uniq + allsynapses.delete_if do |synapse| synapse_ids.index(synapse.id) != nil end diff --git a/app/models/topic.rb b/app/models/topic.rb index 0f312823..62f8b786 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -5,8 +5,8 @@ class Topic < ActiveRecord::Base has_many :synapses1, :class_name => 'Synapse', :foreign_key => 'node1_id', dependent: :destroy has_many :synapses2, :class_name => 'Synapse', :foreign_key => 'node2_id', dependent: :destroy - has_many :topics1, :through => :synapses2, :source => :topic1 - has_many :topics2, :through => :synapses1, :source => :topic2 + has_many :topics1, :through => :synapses2, source: :topic1 + has_many :topics2, :through => :synapses1, source: :topic2 has_many :mappings, as: :mappable, dependent: :destroy has_many :maps, :through => :mappings @@ -41,10 +41,15 @@ class Topic < ActiveRecord::Base belongs_to :metacode - scope :relatives, ->(topic_id = nil) { - includes(:synapses1) - .includes(:synapses2) - .where('synapses.node1_id = ? OR synapses.node2_id = ?', topic_id, topic_id) + scope :relatives1, ->(topic_id = nil) { + includes(:topics1) + .where('synapses.node1_id = ?', topic_id) + .references(:synapses) + } + + scope :relatives2, ->(topic_id = nil) { + includes(:topics2) + .where('synapses.node2_id = ?', topic_id) .references(:synapses) } From ac9460be77c22e12516a2240404ef319d0b34c49 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 14 Mar 2016 21:44:50 +1100 Subject: [PATCH 269/305] do still need cancan --- Gemfile | 1 + Gemfile.lock | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/Gemfile b/Gemfile index 497172e4..bf5997af 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ gem 'devise' gem 'redis' gem 'pg' gem 'pundit' +gem 'cancan' gem 'pundit_extra' gem 'formula' gem 'formtastic' diff --git a/Gemfile.lock b/Gemfile.lock index 86fea5e0..ff5c1317 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -57,6 +57,7 @@ GEM debug_inspector (>= 0.0.1) builder (3.2.2) byebug (8.2.2) + cancan (1.6.10) cancancan (1.10.1) climate_control (0.0.3) activesupport (>= 3.0) @@ -264,6 +265,7 @@ DEPENDENCIES best_in_place better_errors binding_of_caller + cancan coffee-rails delayed_job (~> 4.0.2) delayed_job_active_record (~> 4.0.1) @@ -299,3 +301,6 @@ DEPENDENCIES tunemygc uglifier uservoice-ruby + +BUNDLED WITH + 1.11.2 From 756fe75664bccea20f24eb694f8d3c177395a77e Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 29 Feb 2016 10:46:42 +0800 Subject: [PATCH 270/305] call for developers in Inspect Element window --- app/views/layouts/application.html.erb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 0480cb39..c5f653b6 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -5,6 +5,17 @@ # displayed within, based on URL #%> + + From 5ea61341ebd301c042438a80ce82c574ad0b5e4b Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 15 Mar 2016 16:16:19 +0800 Subject: [PATCH 271/305] add rails intro for newcomers --- doc/RailsIntroduction.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 doc/RailsIntroduction.md diff --git a/doc/RailsIntroduction.md b/doc/RailsIntroduction.md new file mode 100644 index 00000000..7fd7021d --- /dev/null +++ b/doc/RailsIntroduction.md @@ -0,0 +1,38 @@ +# How does Ruby on Rails work? + +Ruby on Rails is a pretty intimidating framework to get started with, since there are so many files. Here's a quick rundown on getting started: + +1. Where should I look for code? +2. How do I know what code generates what pages of metamaps.cc? + +## Where should I look for code? + +Here are the top level folders you should know about: + +- app: holds the ruby code + assets that make up the app. Really, you only need to look in here to see how the app works. +- spec: tests describing how the code *should* work +- db: code for handling interaction with the underlying Postgresql database +- config: low-level, in-depth configuration variables. +- Gemfile: listing of app dependencies from https://rubygems.org/ +- realtime: code for our Node.JS realtime server. This is a separate server written in Javascript that isn't served by ruby on rails. + +Within the app/ folder, you can find these important folders: + +- models: files describing the logic surrounding maps, topics, synapses and more in the framework +- views: HTML template files that allow you to generate HTML using ruby code +- helpers: globally accessible helper functions available to views; they help us take logic out of the view files +- controllers: functions that map a route (e.g. `GET https://metamaps.cc/maps/2`) to a controller action (e.g. maps_controller.rb's `show` function). +- services: files that encapsulate a certain feature or logic into one file that can be referenced. Usually services help us take logic out of models and controllers. +- assets/stylesheets: CSS stylesheets for look and feel +- assets/javascripts: This is a huge folder, containing all of our Javascript code. This folder itself is at least as important as the rest of the repository. + +## How do I know what code generates what pages of metamaps.cc? + +The lifecycle works something like this. + +1. run `rake routes` inside the metamaps_gen002 directory on your computer, and it will generate a list with entries looking something like `GET /maps/:id maps#show`. This tells you which URL will end up at which *controller*. In this example, if you accessed `https://metamaps.cc/maps/2`, you are looking for the maps_controller's `show` function, and there will be a variable params["id"] that is equal to 2. +2. Now in `app/controllers/maps_controller.rb`, you can find the function. It should do some calculations, create an instance variable @map, and then do one of two things: + - If it doesn't call anything, ruby on rails will automatically load app/views/map/show.html.erb. (NB: If you loaded `/maps/2.json`, it would look for app/views/map/show.json.erb). Any instance variables assigned (e.g. @map) will be available to the view file (show.html.erb). + - You can also call the render function directly. See the codebase or http://guides.rubyonrails.org/layouts_and_rendering.html#using-render for details. +3. The map's show template (show.html.erb) will contain actual HTML, which gets us a lot closer to an HTML page. Ruby on rails will fill in a "layout" from app/views/layouts to wrap the content of the page. It will also let you include code with `<% %>` (for logical operations) or `<%= %>` (to print a ruby string directly to the HTML page). The view may refer to attributes on the @map object passed from the controller. For more details on how the @map object works, you can check its definition in app/models/map.rb. +4. The shortest possible rails model file would look like this: `class Map < ActiveRecord::Base; end`. In this case, rails would look for a database table called "maps" and allow access to the columns. For instance, a postgresql INTEGER column called "id" would be accessible as @map.id. However, you can also specify validations, shorthand queries called scopes, and helper functions that specify the logic of the model. It is generally preferable to put logic in the model rather than in a controller or view, so these files are excellent sources of information about how the app works. From 8b54e53743dd72eb47766b63ae2beef7c1e5cbda Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Wed, 16 Mar 2016 11:39:24 +0800 Subject: [PATCH 272/305] metacode icon tests --- spec/models/metacode_spec.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/spec/models/metacode_spec.rb b/spec/models/metacode_spec.rb index 6e6435b0..fc31e3e1 100644 --- a/spec/models/metacode_spec.rb +++ b/spec/models/metacode_spec.rb @@ -3,4 +3,21 @@ require 'rails_helper' RSpec.describe Metacode, type: :model do it { is_expected.to have_many :topics } it { is_expected.to have_many :metacode_sets } + + context 'BOTH aws_icon and manual_icon' do + let(:icon) { File.open(Rails.root.join('app', 'assets', 'images', + 'user.png')) } + let(:metacode) { build(:metacode, aws_icon: icon, + manual_icon: 'https://metamaps.cc/assets/user.png') } + it 'raises a validation error' do + expect { metacode.save! }.to raise_error ActiveRecord::RecordInvalid + end + end + + context 'NEITHER aws_icon or manual_icon' do + let(:metacode2) { build(:metacode, aws_icon: nil, manual_icon: nil) } + it 'raises a validation error' do + expect { metacode2.save! }.to raise_error ActiveRecord::RecordInvalid + end + end end From a0d38c8fb8da8bef74a7d1377bef892d7b6204b0 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Wed, 16 Mar 2016 11:42:51 +0800 Subject: [PATCH 273/305] check https manual icons --- spec/models/metacode_spec.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spec/models/metacode_spec.rb b/spec/models/metacode_spec.rb index fc31e3e1..ad0b6ced 100644 --- a/spec/models/metacode_spec.rb +++ b/spec/models/metacode_spec.rb @@ -15,9 +15,16 @@ RSpec.describe Metacode, type: :model do end context 'NEITHER aws_icon or manual_icon' do - let(:metacode2) { build(:metacode, aws_icon: nil, manual_icon: nil) } + let(:metacode) { build(:metacode, aws_icon: nil, manual_icon: nil) } it 'raises a validation error' do - expect { metacode2.save! }.to raise_error ActiveRecord::RecordInvalid + expect { metacode.save! }.to raise_error ActiveRecord::RecordInvalid + end + end + + context 'non-https manual icon' do + let(:metacode) { build(:metacode, manual_icon: 'http://metamaps.cc/assets/user.png') } + it 'raises a validation error' do + expect { metacode.save! }.to raise_error ActiveRecord::RecordInvalid end end end From 0c3010be90f42a352df37fd58e8c15d016d0eccc Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Wed, 16 Mar 2016 11:45:12 +0800 Subject: [PATCH 274/305] Small revisions to rails intro --- doc/RailsIntroduction.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/RailsIntroduction.md b/doc/RailsIntroduction.md index 7fd7021d..b0734417 100644 --- a/doc/RailsIntroduction.md +++ b/doc/RailsIntroduction.md @@ -9,10 +9,10 @@ Ruby on Rails is a pretty intimidating framework to get started with, since ther Here are the top level folders you should know about: -- app: holds the ruby code + assets that make up the app. Really, you only need to look in here to see how the app works. +- app: holds the ruby code + assets that make up the app. This is the only directory you really need to see how the app works. - spec: tests describing how the code *should* work - db: code for handling interaction with the underlying Postgresql database -- config: low-level, in-depth configuration variables. +- config: low-level, in-depth configuration variables. The most interesting file is `config/routes.rb`. - Gemfile: listing of app dependencies from https://rubygems.org/ - realtime: code for our Node.JS realtime server. This is a separate server written in Javascript that isn't served by ruby on rails. From e6ac4b1dcba764d5c75e3e2e9c713e86b1b5e820 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 19 Mar 2016 13:28:55 +1100 Subject: [PATCH 275/305] make metamaps an oauth provider --- Gemfile | 1 + Gemfile.lock | 3 + config/initializers/doorkeeper.rb | 104 +++++++++++++++ config/locales/doorkeeper.en.yml | 123 ++++++++++++++++++ config/routes.rb | 1 + ...20160318141618_create_doorkeeper_tables.rb | 50 +++++++ db/schema.rb | 42 +++++- 7 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 config/initializers/doorkeeper.rb create mode 100644 config/locales/doorkeeper.en.yml create mode 100644 db/migrate/20160318141618_create_doorkeeper_tables.rb diff --git a/Gemfile b/Gemfile index bf5997af..e0105106 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,7 @@ gem 'pg' gem 'pundit' gem 'cancan' gem 'pundit_extra' +gem 'doorkeeper' gem 'formula' gem 'formtastic' gem 'json' diff --git a/Gemfile.lock b/Gemfile.lock index ff5c1317..3a3c763b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -87,6 +87,8 @@ GEM warden (~> 1.2.3) diff-lcs (1.2.5) docile (1.1.5) + doorkeeper (3.1.0) + railties (>= 3.2) dotenv (2.1.0) erubis (2.7.0) execjs (2.6.0) @@ -270,6 +272,7 @@ DEPENDENCIES delayed_job (~> 4.0.2) delayed_job_active_record (~> 4.0.1) devise + doorkeeper dotenv factory_girl_rails formtastic diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb new file mode 100644 index 00000000..10b9980b --- /dev/null +++ b/config/initializers/doorkeeper.rb @@ -0,0 +1,104 @@ +Doorkeeper.configure do + # Change the ORM that doorkeeper will use (needs plugins) + orm :active_record + + # This block will be called to check whether the resource owner is authenticated or not. + resource_owner_authenticator do + current_user || redirect_to(new_user_session_url) + end + + # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. + # admin_authenticator do + # # Put your admin authentication logic here. + # # Example implementation: + # Admin.find_by_id(session[:admin_id]) || redirect_to(new_admin_session_url) + # end + + # Authorization Code expiration time (default 10 minutes). + # authorization_code_expires_in 10.minutes + + # Access token expiration time (default 2 hours). + # If you want to disable expiration, set this to nil. + # access_token_expires_in 2.hours + + # Assign a custom TTL for implicit grants. + # custom_access_token_expires_in do |oauth_client| + # oauth_client.application.additional_settings.implicit_oauth_expiration + # end + + # Use a custom class for generating the access token. + # https://github.com/doorkeeper-gem/doorkeeper#custom-access-token-generator + # access_token_generator "::Doorkeeper::JWT" + + # Reuse access token for the same resource owner within an application (disabled by default) + # Rationale: https://github.com/doorkeeper-gem/doorkeeper/issues/383 + # reuse_access_token + + # Issue access tokens with refresh token (disabled by default) + # use_refresh_token + + # Provide support for an owner to be assigned to each registered application (disabled by default) + # Optional parameter :confirmation => true (default false) if you want to enforce ownership of + # a registered application + # Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support + # enable_application_owner :confirmation => false + + # Define access token scopes for your provider + # For more information go to + # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes + # default_scopes :public + # optional_scopes :write, :update + + # Change the way client credentials are retrieved from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:client_id` and `:client_secret` params from the `params` object. + # Check out the wiki for more information on customization + # client_credentials :from_basic, :from_params + + # Change the way access token is authenticated from the request object. + # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then + # falls back to the `:access_token` or `:bearer_token` params from the `params` object. + # Check out the wiki for more information on customization + # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param + + # Change the native redirect uri for client apps + # When clients register with the following redirect uri, they won't be redirected to any server and the authorization code will be displayed within the provider + # The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL + # (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi) + # + # native_redirect_uri 'urn:ietf:wg:oauth:2.0:oob' + + # Forces the usage of the HTTPS protocol in non-native redirect uris (enabled + # by default in non-development environments). OAuth2 delegates security in + # communication to the HTTPS protocol so it is wise to keep this enabled. + # + # force_ssl_in_redirect_uri !Rails.env.development? + + # Specify what grant flows are enabled in array of Strings. The valid + # strings and the flows they enable are: + # + # "authorization_code" => Authorization Code Grant Flow + # "implicit" => Implicit Grant Flow + # "password" => Resource Owner Password Credentials Grant Flow + # "client_credentials" => Client Credentials Grant Flow + # + # If not specified, Doorkeeper enables authorization_code and + # client_credentials. + # + # implicit and password grant flows have risks that you should understand + # before enabling: + # http://tools.ietf.org/html/rfc6819#section-4.4.2 + # http://tools.ietf.org/html/rfc6819#section-4.4.3 + # + # grant_flows %w(authorization_code client_credentials) + + # Under some circumstances you might want to have applications auto-approved, + # so that the user skips the authorization step. + # For example if dealing with a trusted application. + # skip_authorization do |resource_owner, client| + # client.superapp? or resource_owner.admin? + # end + + # WWW-Authenticate Realm (default "Doorkeeper"). + # realm "Doorkeeper" +end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml new file mode 100644 index 00000000..7d2d215d --- /dev/null +++ b/config/locales/doorkeeper.en.yml @@ -0,0 +1,123 @@ +en: + activerecord: + attributes: + doorkeeper/application: + name: 'Name' + redirect_uri: 'Redirect URI' + errors: + models: + doorkeeper/application: + attributes: + redirect_uri: + fragment_present: 'cannot contain a fragment.' + invalid_uri: 'must be a valid URI.' + relative_uri: 'must be an absolute URI.' + secured_uri: 'must be an HTTPS/SSL URI.' + + doorkeeper: + applications: + confirmations: + destroy: 'Are you sure?' + buttons: + edit: 'Edit' + destroy: 'Destroy' + submit: 'Submit' + cancel: 'Cancel' + authorize: 'Authorize' + form: + error: 'Whoops! Check your form for possible errors' + help: + redirect_uri: 'Use one line per URI' + native_redirect_uri: 'Use %{native_redirect_uri} for local tests' + scopes: 'Separate scopes with spaces. Leave blank to use the default scopes.' + edit: + title: 'Edit application' + index: + title: 'Your applications' + new: 'New Application' + name: 'Name' + callback_url: 'Callback URL' + new: + title: 'New Application' + show: + title: 'Application: %{name}' + application_id: 'Application Id' + secret: 'Secret' + scopes: 'Scopes' + callback_urls: 'Callback urls' + actions: 'Actions' + + authorizations: + buttons: + authorize: 'Authorize' + deny: 'Deny' + error: + title: 'An error has occurred' + new: + title: 'Authorization required' + prompt: 'Authorize %{client_name} to use your account?' + able_to: 'This application will be able to' + show: + title: 'Authorization code' + + authorized_applications: + confirmations: + revoke: 'Are you sure?' + buttons: + revoke: 'Revoke' + index: + title: 'Your authorized applications' + application: 'Application' + created_at: 'Created At' + date_format: '%Y-%m-%d %H:%M:%S' + + errors: + messages: + # Common error messages + invalid_request: 'The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed.' + invalid_redirect_uri: 'The redirect uri included is not valid.' + unauthorized_client: 'The client is not authorized to perform this request using this method.' + access_denied: 'The resource owner or authorization server denied the request.' + invalid_scope: 'The requested scope is invalid, unknown, or malformed.' + server_error: 'The authorization server encountered an unexpected condition which prevented it from fulfilling the request.' + temporarily_unavailable: 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.' + + #configuration error messages + credential_flow_not_configured: 'Resource Owner Password Credentials flow failed due to Doorkeeper.configure.resource_owner_from_credentials being unconfigured.' + resource_owner_authenticator_not_configured: 'Resource Owner find failed due to Doorkeeper.configure.resource_owner_authenticator being unconfiged.' + + # Access grant errors + unsupported_response_type: 'The authorization server does not support this response type.' + + # Access token errors + invalid_client: 'Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method.' + invalid_grant: 'The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client.' + unsupported_grant_type: 'The authorization grant type is not supported by the authorization server.' + + # Password Access token errors + invalid_resource_owner: 'The provided resource owner credentials are not valid, or resource owner cannot be found' + + invalid_token: + revoked: "The access token was revoked" + expired: "The access token expired" + unknown: "The access token is invalid" + + flash: + applications: + create: + notice: 'Application created.' + destroy: + notice: 'Application deleted.' + update: + notice: 'Application updated.' + authorized_applications: + destroy: + notice: 'Application revoked.' + + layouts: + admin: + nav: + oauth2_provider: 'OAuth2 Provider' + applications: 'Applications' + application: + title: 'OAuth authorization required' diff --git a/config/routes.rb b/config/routes.rb index b9be5288..013bcd8b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,6 @@ Metamaps::Application.routes.draw do + use_doorkeeper root to: 'main#home', via: :get get 'request', to: 'main#requestinvite', as: :request diff --git a/db/migrate/20160318141618_create_doorkeeper_tables.rb b/db/migrate/20160318141618_create_doorkeeper_tables.rb new file mode 100644 index 00000000..d89b005c --- /dev/null +++ b/db/migrate/20160318141618_create_doorkeeper_tables.rb @@ -0,0 +1,50 @@ +class CreateDoorkeeperTables < ActiveRecord::Migration + def change + create_table :oauth_applications do |t| + t.string :name, null: false + t.string :uid, null: false + t.string :secret, null: false + t.text :redirect_uri, null: false + t.string :scopes, null: false, default: '' + t.timestamps + end + + add_index :oauth_applications, :uid, unique: true + + create_table :oauth_access_grants do |t| + t.integer :resource_owner_id, null: false + t.integer :application_id, null: false + t.string :token, null: false + t.integer :expires_in, null: false + t.text :redirect_uri, null: false + t.datetime :created_at, null: false + t.datetime :revoked_at + t.string :scopes + end + + add_index :oauth_access_grants, :token, unique: true + + create_table :oauth_access_tokens do |t| + t.integer :resource_owner_id + t.integer :application_id + + # If you use a custom token generator you may need to change this column + # from string to text, so that it accepts tokens larger than 255 + # characters. More info on custom token generators in: + # https://github.com/doorkeeper-gem/doorkeeper/tree/v3.0.0.rc1#custom-access-token-generator + # + # t.text :token, null: false + t.string :token, null: false + + t.string :refresh_token + t.integer :expires_in + t.datetime :revoked_at + t.datetime :created_at, null: false + t.string :scopes + end + + add_index :oauth_access_tokens, :token, unique: true + add_index :oauth_access_tokens, :resource_owner_id + add_index :oauth_access_tokens, :refresh_token, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index d0381055..1ad745a2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160313003721) do +ActiveRecord::Schema.define(version: 20160318141618) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -132,6 +132,46 @@ ActiveRecord::Schema.define(version: 20160313003721) do t.datetime "aws_icon_updated_at" end + create_table "oauth_access_grants", force: :cascade do |t| + t.integer "resource_owner_id", null: false + t.integer "application_id", null: false + t.string "token", null: false + t.integer "expires_in", null: false + t.text "redirect_uri", null: false + t.datetime "created_at", null: false + t.datetime "revoked_at" + t.string "scopes" + end + + add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree + + create_table "oauth_access_tokens", force: :cascade do |t| + t.integer "resource_owner_id" + t.integer "application_id" + t.string "token", null: false + t.string "refresh_token" + t.integer "expires_in" + t.datetime "revoked_at" + t.datetime "created_at", null: false + t.string "scopes" + end + + add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree + add_index "oauth_access_tokens", ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree + add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree + + create_table "oauth_applications", force: :cascade do |t| + t.string "name", null: false + t.string "uid", null: false + t.string "secret", null: false + t.text "redirect_uri", null: false + t.string "scopes", default: "", null: false + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree + create_table "synapses", force: :cascade do |t| t.text "desc" t.text "category" From 617dec72b9bede9b8034a38d1a254f655e0b5e0c Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 21 Mar 2016 13:08:34 -0700 Subject: [PATCH 276/305] hide sound option. green 'in call' dot. tooltip --- app/assets/javascripts/src/views/chatView.js.erb | 4 ++-- app/assets/stylesheets/clean.css.erb | 8 ++++++-- app/assets/stylesheets/junto.css.erb | 9 +++++++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/src/views/chatView.js.erb b/app/assets/javascripts/src/views/chatView.js.erb index 0ba86ee6..605c15ed 100644 --- a/app/assets/javascripts/src/views/chatView.js.erb +++ b/app/assets/javascripts/src/views/chatView.js.erb @@ -17,7 +17,7 @@ Metamaps.Views.chatView = (function () { "
    {{ username }} {{ selfName }}
    " + "" + "" + - "IN CALL" + + "
    " + "
    " + "
    ", templates: function() { @@ -30,7 +30,7 @@ Metamaps.Views.chatView = (function () { }, createElements: function() { this.$unread = $('
    '); - this.$button = $('
    '); + this.$button = $('
    Chat
    '); this.$messageInput = $(''); this.$juntoHeader = $('
    PARTICIPANTS
    '); this.$videoToggle = $('
    '); diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index 09b5eba8..5665716a 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -456,7 +456,7 @@ } .zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .takeScreenshot:hover .tooltips, .sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder, - .mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove { + .mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips { display: block; } @@ -536,6 +536,10 @@ left: -11px; } +.chat-button .tooltips { + top: 10px; +} + .openCheatsheet .tooltipsAbove { left: -4px; } @@ -545,7 +549,7 @@ margin-top: 40px; } -.zoomExtents div::after, .zoomIn div::after, .zoomOut div::after, .takeScreenshot div:after { +.zoomExtents div::after, .zoomIn div::after, .zoomOut div::after, .takeScreenshot div:after, .chat-button div.tooltips::after { content: ''; position: absolute; top: 57%; diff --git a/app/assets/stylesheets/junto.css.erb b/app/assets/stylesheets/junto.css.erb index ed9b62b5..71741e16 100644 --- a/app/assets/stylesheets/junto.css.erb +++ b/app/assets/stylesheets/junto.css.erb @@ -215,7 +215,6 @@ width: 32px; height: 32px; border-radius: 18px; - cursor: pointer; } .chat-box .participants .participant .chat-participant-name { width: 53%; @@ -254,6 +253,12 @@ display: none; margin-top: 14px; } +.chat-box .participants .participant .chat-participant-participating .green-dot { + background: #4fc059; + width: 12px; + height: 12px; + border-radius: 6px; +} .chat-box .participants .participant.active .chat-participant-participating { display: block; } @@ -268,6 +273,7 @@ box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19); } .chat-box .chat-header .sound-toggle { + display: none; width: 24px; height: 24px; margin-right: 32px; @@ -327,7 +333,6 @@ width: 32px; height: 32px; border-radius: 18px; - cursor: pointer; } .chat-box .chat-messages .chat-message .chat-message-text { width: 73%; From baa5439f0f07eb2fb83a314a8f241d691a89ba42 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Wed, 23 Mar 2016 16:12:23 -0700 Subject: [PATCH 277/305] auto position videos which haven't been manually positioned --- app/assets/javascripts/src/Metamaps.js.erb | 72 ++++++++++++++----- app/assets/javascripts/src/views/room.js | 2 +- app/assets/javascripts/src/views/videoView.js | 21 +----- 3 files changed, 57 insertions(+), 38 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index c0f2daef..2eb08b70 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -1933,7 +1933,6 @@ Metamaps.Realtime = { chatOpen: false, status: true, // stores whether realtime is True/On or False/Off, broadcastingStatus: false, - videosInPosition: 1, inConversation: false, localVideo: null, init: function () { @@ -2036,23 +2035,62 @@ Metamaps.Realtime = { }, handleVideoAdded: function (v, id) { var self = Metamaps.Realtime; - // random position for now - var paddingAboveTop = 72; - var heightOfVideo = 150; - var padding = 25; - var top = paddingAboveTop + (self.videosInPosition * heightOfVideo) + padding; - var left = 30; - //var right = Math.floor((Math.random() * (468 - 100)) + 1); + self.positionVideos(); v.setParent($('#wrapper')); - $('#wrapper').append(v.$container); - v.$container.css({ - top: top + 'px', - left: left + 'px' - }); v.$container.find('.video-cutoff').css({ border: '4px solid ' + self.mappersOnMap[id].color }); - self.videosInPosition += 1; + $('#wrapper').append(v.$container); + }, + positionVideos: function () { + var self = Metamaps.Realtime; + var videoIds = Object.keys(self.room.videos); + var numOfVideos = videoIds.length; + var numOfVideosToPosition = _.filter(videoIds, function (id) { + return !self.room.videos[id].manuallyPositioned; + }).length; + + var screenHeight = $(document).height(); + var screenWidth = $(document).width(); + var topExtraPadding = 20; + var topPadding = 30; + var leftPadding = 30; + var videoHeight = 150; + var videoWidth = 180; + var column = 0; + var row = 0; + var yFormula = function () { + var y = topExtraPadding + (topPadding + videoHeight)*row + topPadding; + if (y + videoHeight > screenHeight) { + row = 0; + column += 1; + y = yFormula(); + } + row++; + return y; + }; + var xFormula = function () { + var x = (leftPadding + videoWidth)*column + leftPadding; + return x; + }; + + // do self first + var myVideo = Metamaps.Realtime.localVideo.view; + if (!myVideo.manuallyPositioned) { + myVideo.$container.css({ + top: yFormula() + 'px', + left: xFormula() + 'px' + }); + } + videoIds.forEach(function (id) { + var video = self.room.videos[id]; + if (!video.manuallyPositioned) { + video.$container.css({ + top: yFormula() + 'px', + left: xFormula() + 'px' + }); + } + }); }, startActiveMap: function () { var self = Metamaps.Realtime; @@ -2083,7 +2121,6 @@ Metamaps.Realtime = { self.room.leave(); self.room.chat.$container.hide(); self.room.chat.close(); - self.videosInPosition = 1; }, reenableRealtime: function() { var confirmString = "The layout of your map has fallen out of sync with the saved copy. "; @@ -2195,7 +2232,6 @@ Metamaps.Realtime = { self.room.conversationEnding(); self.room.leaveVideoOnly(); self.inConversation = false; - self.videosInPosition = 1; self.localVideo.view.$container.hide().css({ top: '72px', left: '30px' @@ -2303,6 +2339,8 @@ Metamaps.Realtime = { self.webrtc.once('readyToCall', function () { self.videoInitialized = true; self.readyToCall = true; + self.localVideo.view.manuallyPositioned = false; + self.positionVideos(); self.localVideo.view.$container.show(); if (self.localVideo && self.status) { $('#wrapper').append(self.localVideo.view.$container); @@ -2329,7 +2367,6 @@ Metamaps.Realtime = { self.room.chat.mapperLeftCall(Metamaps.Active.Mapper.id); self.room.leaveVideoOnly(); self.inConversation = false; - self.videosInPosition = 1; self.localVideo.view.$container.hide(); // if there's only two people in the room, and we're leaving @@ -4053,6 +4090,7 @@ Metamaps.Listeners = { $(window).resize(function () { if (Metamaps.Visualize && Metamaps.Visualize.mGraph) Metamaps.Visualize.mGraph.canvas.resize($(window).width(), $(window).height()); if ((Metamaps.Active.Map || Metamaps.Active.Topic) && Metamaps.Famous && Metamaps.Famous.maps.surf) Metamaps.Famous.maps.reposition(); + if (Metamaps.Active.Map && Metamaps.Realtime.inConversation) Metamaps.Realtime.positionVideos(); }); } }; // end Metamaps.Listeners diff --git a/app/assets/javascripts/src/views/room.js b/app/assets/javascripts/src/views/room.js index e54dfb00..d45b6b26 100644 --- a/app/assets/javascripts/src/views/room.js +++ b/app/assets/javascripts/src/views/room.js @@ -141,8 +141,8 @@ Metamaps.Views.room = (function () { var v = new VideoView(video, null, id, false, { DOUBLE_CLICK_TOLERANCE: 200, avatar: peer.avatar, username: peer.username }); - if (this._videoAdded) this._videoAdded(v, peer.nick); this.videos[peer.id] = v; + if (this._videoAdded) this._videoAdded(v, peer.nick); } room.prototype.removeVideo = function (peer) { diff --git a/app/assets/javascripts/src/views/videoView.js b/app/assets/javascripts/src/views/videoView.js index 629dc2e0..b9d39c06 100644 --- a/app/assets/javascripts/src/views/videoView.js +++ b/app/assets/javascripts/src/views/videoView.js @@ -67,6 +67,7 @@ Metamaps.Views.videoView = (function () { newY; if (this.$parent && this.mouseIsDown) { + this.manuallyPositioned = true; this.hasMoved = true; diffX = event.pageX - this.mouseMoveStart.x; diffY = this.mouseMoveStart.y - event.pageY; @@ -94,14 +95,6 @@ Metamaps.Views.videoView = (function () { } $(document).trigger(videoView.events.videoControlClick, [this]); }, - /*yesReceiveClick: function () { - this.$receiveContainer.hide(); - this.$avatar.hide(); - $(this.video).prop('muted', false); - }, - noReceiveClick: function () { - this.$container.hide(); - }*/ }; var videoView = function(video, $parent, id, isMyself, config) { @@ -131,18 +124,6 @@ Metamaps.Views.videoView = (function () { $vidContainer.addClass('video-cutoff'); $vidContainer.append(this.video); - /* - if (!isMyself) { - this.$receiveContainer = $('
    ' + config.username + ' is sharing their audio and video. Do you wish to receive it?
    '); - this.$container.append(this.$receiveContainer); - this.$container.find('.btn-yes').on('click', function (event) { - Handlers.yesReceiveClick.call(self, event); - }); - this.$container.find('.btn-no').on('click', function (event) { - Handlers.noReceiveClick.call(self, event); - }); - }*/ - this.avatar = config.avatar; this.$avatar = $(''); $vidContainer.append(this.$avatar); From c4890274f22c7a495d19433c2872c656d09eae91 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Wed, 23 Mar 2016 16:29:26 -0700 Subject: [PATCH 278/305] switch messages to use pundit --- app/controllers/messages_controller.rb | 9 +++++-- app/models/message.rb | 35 ------------------------- app/policies/message_policy.rb | 36 ++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 37 deletions(-) create mode 100644 app/policies/message_policy.rb diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index e47f9e38..386919f3 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -1,10 +1,12 @@ class MessagesController < ApplicationController - before_filter :require_user, except: [:show] + before_action :require_user, except: [:show] + after_action :verify_authorized # GET /messages/1.json def show @message = Message.find(params[:id]) + authorize @message respond_to do |format| format.json { render json: @message } @@ -15,8 +17,8 @@ class MessagesController < ApplicationController # POST /messages.json def create @message = Message.new(message_params) - @message.user = current_user + authorize @message respond_to do |format| if @message.save @@ -31,6 +33,7 @@ class MessagesController < ApplicationController # PUT /messages/1.json def update @message = Message.find(params[:id]) + authorize @message respond_to do |format| if @message.update_attributes(message_params) @@ -45,6 +48,8 @@ class MessagesController < ApplicationController # DELETE /messages/1.json def destroy @message = Message.find(params[:id]) + authorize @message + @message.destroy respond_to do |format| diff --git a/app/models/message.rb b/app/models/message.rb index ca9d8553..0481192f 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -16,39 +16,4 @@ class Message < ActiveRecord::Base json end - ##### PERMISSIONS ###### - - def authorize_to_delete(user) - if (self.user != user) - return false - end - return self - end - - # returns false if user not allowed to 'show' Topic, Synapse, or Map - def authorize_to_show(user) - if (self.resource && self.resource.permission == "private" && self.resource.user != user) - return false - end - return self - end - - # returns false if user not allowed to 'edit' Topic, Synapse, or Map - def authorize_to_edit(user) - if !user - return false - elsif (self.user != user) - return false - end - return self - end - - # returns Boolean if user allowed to view Topic, Synapse, or Map - def authorize_to_view(user) - if (self.resource && self.resource.permission == "private" && self.resource.user != user) - return false - end - return true - end - end diff --git a/app/policies/message_policy.rb b/app/policies/message_policy.rb new file mode 100644 index 00000000..af2efb0c --- /dev/null +++ b/app/policies/message_policy.rb @@ -0,0 +1,36 @@ +class MessagePolicy < ApplicationPolicy + class Scope < Scope + def resolve + visible = ['public', 'commons'] + permission = 'maps.permission IN (?)' + if user + scope.joins(:maps).where(permission + ' OR maps.user_id = ?', visible, user.id) + else + scope.where(permission, visible) + end + end + end + + def show? + resource_policy.show? + end + + def create? + record.resource.present? && resource_policy.update? + end + + def update? + record.user == user + end + + def destroy? + record.user == user || admin_override + end + + # Helpers + + def resource_policy + @resource_policy ||= Pundit.policy(user, record.resource) + end + +end From 70bc0959b006b9d32d3a38f64d722aecd6b6483f Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Wed, 23 Mar 2016 16:50:53 -0700 Subject: [PATCH 279/305] update version, date, and peers --- app/views/layouts/_lightboxes.html.erb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/layouts/_lightboxes.html.erb b/app/views/layouts/_lightboxes.html.erb index f9bb78ff..d6e8010a 100644 --- a/app/views/layouts/_lightboxes.html.erb +++ b/app/views/layouts/_lightboxes.html.erb @@ -18,8 +18,8 @@

    PRIVATE BETA

    -

    2.8

    -

    Nov 29, 2014

    +

    2.9

    +

    Mar 25, 2016

    @@ -73,8 +73,8 @@

    Metamaps.cc is an initiative of affiliates of the Metamaps Open Value Network, an evolving network of peers working together to create shared value. Metamaps continues to be a constantly developing experiment in progress.

    The peers behind Metamaps have their own respective backgrounds as artists, designers, engineers, advocates, activitists, filmmakers and systems thinkers and share a deep commitment to the creative culture they support.

    -

    ACTIVE PEERS (as of 11/2014)

    -

    Robert Best, Benjamin Brownell, Marija Coneva, Devin Howard, Bashar Jabbour, Raymon Johnstone, Shai Mor, Ishan Shapiro, Connor Turland

    +

    ACTIVE PEERS (as of 03/2016)

    +

    Robert Best, Benjamin Brownell, Devin Howard, Raymon Johnstone, Ishan Shapiro, Connor Turland

    ICONOGRAPHY

    From 14dfe3c92634ef6b591382beb2d12b91c235068e Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Thu, 24 Mar 2016 17:16:27 -0700 Subject: [PATCH 280/305] styling for doorkeeper and api functional --- .../javascripts/src/Metamaps.GlobalUI.js.erb | 4 +- app/assets/stylesheets/apps.css.erb | 131 ++++++++++++++++++ app/controllers/api/restful_controller.rb | 17 ++- .../applications/_delete_form.html.erb | 5 + .../doorkeeper/applications/_form.html.erb | 36 +++++ .../doorkeeper/applications/_script.html.erb | 10 ++ .../doorkeeper/applications/edit.html.erb | 10 ++ .../doorkeeper/applications/index.html.erb | 28 ++++ .../doorkeeper/applications/new.html.erb | 11 ++ .../doorkeeper/applications/show.html.erb | 37 +++++ .../doorkeeper/authorizations/error.html.erb | 11 ++ .../doorkeeper/authorizations/new.html.erb | 48 +++++++ .../doorkeeper/authorizations/show.html.erb | 11 ++ .../_delete_form.html.erb | 5 + .../authorized_applications/_script.html.erb | 10 ++ .../authorized_applications/index.html.erb | 30 ++++ app/views/layouts/_account.html.erb | 4 +- app/views/layouts/_lightboxes.html.erb | 2 +- app/views/layouts/_upperelements.html.erb | 12 +- app/views/layouts/application.html.erb | 2 +- app/views/layouts/doorkeeper.html.erb | 125 +++++++++++++++++ config/application.rb | 7 + config/initializers/doorkeeper.rb | 10 +- config/locales/doorkeeper.en.yml | 35 +++-- public/famous/main.js | 5 +- public/famous/templates.js | 7 + 26 files changed, 571 insertions(+), 42 deletions(-) create mode 100644 app/assets/stylesheets/apps.css.erb create mode 100644 app/views/doorkeeper/applications/_delete_form.html.erb create mode 100644 app/views/doorkeeper/applications/_form.html.erb create mode 100644 app/views/doorkeeper/applications/_script.html.erb create mode 100644 app/views/doorkeeper/applications/edit.html.erb create mode 100644 app/views/doorkeeper/applications/index.html.erb create mode 100644 app/views/doorkeeper/applications/new.html.erb create mode 100644 app/views/doorkeeper/applications/show.html.erb create mode 100644 app/views/doorkeeper/authorizations/error.html.erb create mode 100644 app/views/doorkeeper/authorizations/new.html.erb create mode 100644 app/views/doorkeeper/authorizations/show.html.erb create mode 100644 app/views/doorkeeper/authorized_applications/_delete_form.html.erb create mode 100644 app/views/doorkeeper/authorized_applications/_script.html.erb create mode 100644 app/views/doorkeeper/authorized_applications/index.html.erb create mode 100644 app/views/layouts/doorkeeper.html.erb diff --git a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb index b8c111af..690bba1f 100644 --- a/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb +++ b/app/assets/javascripts/src/Metamaps.GlobalUI.js.erb @@ -43,7 +43,7 @@ Metamaps.Active = { Metamaps.Maps = {}; $(document).ready(function () { - + function init() { for (var prop in Metamaps) { // this runs the init function within each sub-object on the Metamaps one @@ -55,11 +55,13 @@ $(document).ready(function () { Metamaps[prop].init(); } } + } // initialize the famous ui var callFamous = function(){ if (Metamaps.Famous) { Metamaps.Famous.build(); + init(); } else { setTimeout(callFamous, 100); diff --git a/app/assets/stylesheets/apps.css.erb b/app/assets/stylesheets/apps.css.erb new file mode 100644 index 00000000..e6d75dd7 --- /dev/null +++ b/app/assets/stylesheets/apps.css.erb @@ -0,0 +1,131 @@ +.centerContent { + position: relative; + margin: 92px auto 0 auto; + padding: 20px 0 60px 20px; + width: 760px; + overflow: hidden; + box-shadow: 0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24); + background: #fff; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + border: 1px solid #dcdcdc; + margin-bottom: 10px; + padding: 15px; +} + +.centerContent .page-header { + margin-bottom: 20px; + padding-bottom: 8px; + border-bottom: 1px solid #DCDCDC; +} + +.centerContent .form-group { + margin-bottom: 20px; +} + +.centerContent .inline-button { + display: inline-block; +} + +.centerContent.showApp p { + margin-bottom: 20px; +} + +.centerContent a { + color: #4fb5c0; +} +.centerContent a:hover { + text-decoration: underline; +} + +.centerContent a.button { + color: #FFFFFF; +} + +.centerContent a.button:hover { + text-decoration: none; +} + +.centerContent th { + text-align: left; +} + +.centerContent td { + padding-right: 20px; + padding-bottom: 20px; +} + +.centerContent .link-button { + line-height: 32px; + color: #FFFFFF; +} + +.centerContent .button-margin { + margin-bottom: 20px; +} + +.centerContent .button-margin-top { + margin-top: 20px; +} + +.centerContent a.red-button, .centerContent button.red-button, +.centerContent input[type="submit"].red-button { + color: #c04f4f; + background: transparent; + text-transform: none; +} +.centerContent .red-button:hover { + background: transparent; + color: #9A3E3E !important; +} + +.centerContent input[type="text"] { + font-family: 'din-medium', helvetica, sans-serif; + width: 400px; + height: 32px; + font-size: 14px; + direction: ltr; + -webkit-appearance: none; + appearance: none; + display: inline-block; + margin: 0; + padding: 0 8px; + background: #fff; + border: 1px solid #d9d9d9; + border-top: 1px solid #c0c0c0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 2px; + color: #424242; + letter-spacing: normal; + word-spacing: normal; + text-transform: none; + text-indent: 0px; + text-shadow: none; + display: inline-block; + text-align: start; +} + +.centerContent textarea { + color: #424242; + padding: 8px; + border: 1px solid #d9d9d9; + border-top: 1px solid #c0c0c0; + resize: none; + letter-spacing: normal; + word-spacing: normal; + text-transform: none; + text-indent: 0px; + text-shadow: none; + text-align: start; + font-family: 'din-medium', helvetica, sans-serif; + font-size: 14px; + line-height: 17px; + width: 400px; + box-sizing: border-box; + border-radius: 2px; +} diff --git a/app/controllers/api/restful_controller.rb b/app/controllers/api/restful_controller.rb index 06396e3d..721cd33c 100644 --- a/app/controllers/api/restful_controller.rb +++ b/app/controllers/api/restful_controller.rb @@ -30,18 +30,23 @@ class API::RestfulController < ActionController::Base end def current_user - super || token_user || nil + super || token_user || doorkeeper_user || nil end def token_user - authenticate_with_http_token do |token, options| - access_token = Token.find_by_token(token) - if access_token - @token_user ||= access_token.user - end + token = params[:access_token] + access_token = Token.find_by_token(token) + if access_token + @token_user ||= access_token.user end end + def doorkeeper_user + return unless doorkeeper_token.present? + doorkeeper_render_error unless valid_doorkeeper_token? + @doorkeeper_user ||= User.find(doorkeeper_token.resource_owner_id) + end + def permitted_params @permitted_params ||= PermittedParams.new(params) end diff --git a/app/views/doorkeeper/applications/_delete_form.html.erb b/app/views/doorkeeper/applications/_delete_form.html.erb new file mode 100644 index 00000000..69912ec3 --- /dev/null +++ b/app/views/doorkeeper/applications/_delete_form.html.erb @@ -0,0 +1,5 @@ +<%- submit_btn_css ||= 'button red-button' %> +<%= form_tag oauth_application_path(application) do %> + + <%= submit_tag t('doorkeeper.applications.buttons.destroy'), onclick: "return confirm('#{ t('doorkeeper.applications.confirmations.destroy') }')", class: submit_btn_css %> +<% end %> diff --git a/app/views/doorkeeper/applications/_form.html.erb b/app/views/doorkeeper/applications/_form.html.erb new file mode 100644 index 00000000..dd8bec62 --- /dev/null +++ b/app/views/doorkeeper/applications/_form.html.erb @@ -0,0 +1,36 @@ +<%= form_for application, url: doorkeeper_submit_path(application), html: {class: 'form-horizontal', role: 'form'} do |f| %> + <% if application.errors.any? %> +

    <%= t('doorkeeper.applications.form.error') %>

    + <% end %> + + <%= content_tag :div, class: "form-group#{' has-error' if application.errors[:name].present?}" do %> + <%= f.label :name, class: 'col-sm-2 control-label' %> +
    + <%= f.text_field :name, class: 'form-control' %> + <%= doorkeeper_errors_for application, :name %> +
    + <% end %> + + <%= content_tag :div, class: "form-group#{' has-error' if application.errors[:redirect_uri].present?}" do %> + <%= f.label :redirect_uri, class: 'col-sm-2 control-label' %> +
    + <%= f.text_area :redirect_uri, class: 'form-control' %> + <%= doorkeeper_errors_for application, :redirect_uri %> + + <%= t('doorkeeper.applications.help.redirect_uri') %>. + + <% if Doorkeeper.configuration.native_redirect_uri %> + + <%= raw t('doorkeeper.applications.help.native_redirect_uri', native_redirect_uri: "#{ Doorkeeper.configuration.native_redirect_uri }") %> + + <% end %> +
    + <% end %> + +
    +
    + <%= f.submit t('doorkeeper.applications.buttons.submit'), class: "btn btn-primary" %> + <%= link_to t('doorkeeper.applications.buttons.cancel'), oauth_applications_path, :class => "button link-button red-button", :data => { :bypass => 'true' } %> +
    +
    +<% end %> diff --git a/app/views/doorkeeper/applications/_script.html.erb b/app/views/doorkeeper/applications/_script.html.erb new file mode 100644 index 00000000..970791d4 --- /dev/null +++ b/app/views/doorkeeper/applications/_script.html.erb @@ -0,0 +1,10 @@ + diff --git a/app/views/doorkeeper/applications/edit.html.erb b/app/views/doorkeeper/applications/edit.html.erb new file mode 100644 index 00000000..8f92c3a3 --- /dev/null +++ b/app/views/doorkeeper/applications/edit.html.erb @@ -0,0 +1,10 @@ +
    +
    + + + <%= render 'form', application: @application %> +
    +
    +<%= render 'script' %> diff --git a/app/views/doorkeeper/applications/index.html.erb b/app/views/doorkeeper/applications/index.html.erb new file mode 100644 index 00000000..449d5f5e --- /dev/null +++ b/app/views/doorkeeper/applications/index.html.erb @@ -0,0 +1,28 @@ +
    +
    + + + + + + + + + + + + <% @applications.each do |application| %> + + + + + + <% end %> + +
    <%= t('.name') %><%= t('.callback_url') %>
    <%= link_to application.name, oauth_application_path(application), :data => { :bypass => 'true' } %><%= application.redirect_uri %><%= render 'delete_form', application: application %>
    +<%= link_to t('.new'), new_oauth_application_path, class: 'button link-button', :data => { :bypass => 'true' } %> +
    +
    +<%= render 'script' %> diff --git a/app/views/doorkeeper/applications/new.html.erb b/app/views/doorkeeper/applications/new.html.erb new file mode 100644 index 00000000..ef864959 --- /dev/null +++ b/app/views/doorkeeper/applications/new.html.erb @@ -0,0 +1,11 @@ +
    +
    +<%= link_to t('doorkeeper.applications.buttons.back'), oauth_applications_path(), class: 'button link-button button-margin', :data => { :bypass => 'true' } %> + + +<%= render 'form', application: @application %> +
    +
    +<%= render 'script' %> diff --git a/app/views/doorkeeper/applications/show.html.erb b/app/views/doorkeeper/applications/show.html.erb new file mode 100644 index 00000000..045d16a0 --- /dev/null +++ b/app/views/doorkeeper/applications/show.html.erb @@ -0,0 +1,37 @@ +
    +
    + + <%= link_to t('doorkeeper.applications.buttons.back'), oauth_applications_path(), class: 'button link-button button-margin', :data => { :bypass => 'true' } %> + + + +

    <%= t('.application_id') %>:

    +

    <%= @application.uid %>

    + +

    <%= t('.secret') %>:

    +

    <%= @application.secret %>

    + + +

    <%= t('.callback_urls') %>:

    + + + <% @application.redirect_uri.split.each do |uri| %> + + + + + <% end %> +
    + <%= uri %> + + <%= link_to t('doorkeeper.applications.buttons.authorize'), oauth_authorization_path(client_id: @application.uid, redirect_uri: uri, response_type: 'code'), class: 'button link-button', target: '_blank', :data => { :bypass => 'true' } %> +
    + +
    <%= link_to t('doorkeeper.applications.buttons.edit'), edit_oauth_application_path(@application), class: 'button link-button', :data => { :bypass => 'true' } %>
    + +
    <%= render 'delete_form', application: @application, submit_btn_css: 'button red-button' %>
    +
    +
    +<%= render 'script' %> diff --git a/app/views/doorkeeper/authorizations/error.html.erb b/app/views/doorkeeper/authorizations/error.html.erb new file mode 100644 index 00000000..4d778e29 --- /dev/null +++ b/app/views/doorkeeper/authorizations/error.html.erb @@ -0,0 +1,11 @@ +
    +
    + + +
    +
    <%= @pre_auth.error_response.body[:error_description] %>
    +
    +
    +
    diff --git a/app/views/doorkeeper/authorizations/new.html.erb b/app/views/doorkeeper/authorizations/new.html.erb new file mode 100644 index 00000000..52b2f6a4 --- /dev/null +++ b/app/views/doorkeeper/authorizations/new.html.erb @@ -0,0 +1,48 @@ +
    +
    + + +
    +

    + <%= raw t('.prompt', client_name: "#{ @pre_auth.client.name }") %> +

    + + <% if @pre_auth.scopes.count > 0 %> +
    +

    <%= t('.able_to') %>:

    + +
      + <% @pre_auth.scopes.each do |scope| %> +
    • <%= t scope, scope: [:doorkeeper, :scopes] %>
    • + <% end %> +
    +
    + <% end %> + +
    +
    + <%= form_tag oauth_authorization_path, method: :post do %> + <%= hidden_field_tag :client_id, @pre_auth.client.uid %> + <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %> + <%= hidden_field_tag :state, @pre_auth.state %> + <%= hidden_field_tag :response_type, @pre_auth.response_type %> + <%= hidden_field_tag :scope, @pre_auth.scope %> + <%= submit_tag t('doorkeeper.authorizations.buttons.authorize'), class: "button" %> + <% end %> +
    +
    + <%= form_tag oauth_authorization_path, method: :delete do %> + <%= hidden_field_tag :client_id, @pre_auth.client.uid %> + <%= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri %> + <%= hidden_field_tag :state, @pre_auth.state %> + <%= hidden_field_tag :response_type, @pre_auth.response_type %> + <%= hidden_field_tag :scope, @pre_auth.scope %> + <%= submit_tag t('doorkeeper.authorizations.buttons.deny'), class: "button red-button" %> + <% end %> +
    +
    +
    +
    +
    diff --git a/app/views/doorkeeper/authorizations/show.html.erb b/app/views/doorkeeper/authorizations/show.html.erb new file mode 100644 index 00000000..4079ed56 --- /dev/null +++ b/app/views/doorkeeper/authorizations/show.html.erb @@ -0,0 +1,11 @@ +
    +
    + + +
    + <%= params[:code] %> +
    +
    +
    diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.erb b/app/views/doorkeeper/authorized_applications/_delete_form.html.erb new file mode 100644 index 00000000..27ea3d73 --- /dev/null +++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.erb @@ -0,0 +1,5 @@ +<%- submit_btn_css ||= 'button red-button' %> +<%= form_tag oauth_authorized_application_path(application) do %> + + <%= submit_tag t('doorkeeper.authorized_applications.buttons.revoke'), onclick: "return confirm('#{ t('doorkeeper.authorized_applications.confirmations.revoke') }')", class: submit_btn_css %> +<% end %> diff --git a/app/views/doorkeeper/authorized_applications/_script.html.erb b/app/views/doorkeeper/authorized_applications/_script.html.erb new file mode 100644 index 00000000..5e8bebda --- /dev/null +++ b/app/views/doorkeeper/authorized_applications/_script.html.erb @@ -0,0 +1,10 @@ + diff --git a/app/views/doorkeeper/authorized_applications/index.html.erb b/app/views/doorkeeper/authorized_applications/index.html.erb new file mode 100644 index 00000000..0464c2be --- /dev/null +++ b/app/views/doorkeeper/authorized_applications/index.html.erb @@ -0,0 +1,30 @@ +
    +
    + + +
    + + + + + + + + + + + <% @applications.each do |application| %> + + + + + + <% end %> + +
    <%= t('doorkeeper.authorized_applications.index.application') %><%= t('doorkeeper.authorized_applications.index.created_at') %>
    <%= application.name %><%= application.created_at.strftime(t('doorkeeper.authorized_applications.index.date_format')) %><%= render 'delete_form', application: application %>
    +
    +
    +
    +<%= render 'script' %> diff --git a/app/views/layouts/_account.html.erb b/app/views/layouts/_account.html.erb index 6150eebf..1f228481 100644 --- a/app/views/layouts/_account.html.erb +++ b/app/views/layouts/_account.html.erb @@ -3,9 +3,9 @@ # The inner HTML of the account box that comes up in the bottom left #%> -<% if authenticated? %> +<% if current_user %> <% account = current_user %> - <%= image_tag user.image.url(:sixtyfour), :size => "48x48", :class => "sidebarAccountImage" %> + <%= image_tag account.image.url(:sixtyfour), :size => "48x48", :class => "sidebarAccountImage" %>

    <%= account.name.split[0...1][0] %>

    • diff --git a/app/views/layouts/_lightboxes.html.erb b/app/views/layouts/_lightboxes.html.erb index f9bb78ff..7487adad 100644 --- a/app/views/layouts/_lightboxes.html.erb +++ b/app/views/layouts/_lightboxes.html.erb @@ -222,7 +222,7 @@ <%= render :partial => 'shared/cheatsheet' %>
    - <% if authenticated? %> + <% if current_user %>

    SHARE INVITE

    diff --git a/app/views/layouts/_upperelements.html.erb b/app/views/layouts/_upperelements.html.erb index 8328cb84..91a9955b 100644 --- a/app/views/layouts/_upperelements.html.erb +++ b/app/views/layouts/_upperelements.html.erb @@ -4,7 +4,7 @@
    - <% if authenticated? %> + <% if current_user %>
    Save To New Map
    @@ -38,7 +38,7 @@
    - <% if authenticated? %> + <% if current_user %>
    Create New Map
    @@ -48,9 +48,9 @@ <% if !(controller_name == "sessions" && action_name == "new") %>
    Account
    - <% if user && user.image %> - <%= image_tag user.image.url(:thirtytwo), :size => "32x32" %> - <% elsif !authenticated? %> + <% if current_user && current_user.image %> + <%= image_tag current_user.image.url(:thirtytwo), :size => "32x32" %> + <% elsif !current_user %> SIGN IN
    <% end %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index c5f653b6..5774708e 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -104,7 +104,7 @@
    - <%= render :partial => 'layouts/upperelements' %> + <%= render :partial => 'layouts/upperelements', :locals => { :appsPage => false } %> <%= yield %> diff --git a/app/views/layouts/doorkeeper.html.erb b/app/views/layouts/doorkeeper.html.erb new file mode 100644 index 00000000..76caf7a5 --- /dev/null +++ b/app/views/layouts/doorkeeper.html.erb @@ -0,0 +1,125 @@ +<%# +# @file +# Main application file. Holds scaffolding present on every page. +# Then a certain non-partial view (no _ preceding filename) will be +# displayed within, based on URL +#%> + + + + + + + <%=h yield(:title) %> + <%= csrf_meta_tags %> + + + <%= stylesheet_link_tag "application", :media => "all" %> + <%= javascript_include_tag "application" %> + + + + + + + + + + + + + + <% if devise_error_messages? %> +

    <%= devise_error_messages! %>

    + <% elsif notice %> +

    <%= notice %>

    + <% end %> + + <%= content_tag :div, class: "main" do %> + +
    + + <%= render :partial => 'layouts/upperelements', :locals => {:appsPage => true } %> + + <%= yield %> + +
    +
    +
    + + <% end %> + +<%= render :partial => 'layouts/lightboxes' %> +<%= render :partial => 'layouts/templates' %> +<%= render :partial => 'shared/metacodeBgColors' %> + + +<%= render :partial => 'layouts/googleanalytics' if Rails.env.production? %> + + diff --git a/config/application.rb b/config/application.rb index fb9161a5..0431e58b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -37,6 +37,13 @@ module Metamaps # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" + config.to_prepare do + Doorkeeper::ApplicationsController.layout "doorkeeper" + Doorkeeper::AuthorizationsController.layout "doorkeeper" + Doorkeeper::AuthorizedApplicationsController.layout "doorkeeper" + Doorkeeper::ApplicationController.helper ApplicationHelper + end + # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [:password] diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 10b9980b..843fe831 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -8,18 +8,16 @@ Doorkeeper.configure do end # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. - # admin_authenticator do - # # Put your admin authentication logic here. - # # Example implementation: - # Admin.find_by_id(session[:admin_id]) || redirect_to(new_admin_session_url) - # end + admin_authenticator do + current_user || redirect_to(new_user_session_url) + end # Authorization Code expiration time (default 10 minutes). # authorization_code_expires_in 10.minutes # Access token expiration time (default 2 hours). # If you want to disable expiration, set this to nil. - # access_token_expires_in 2.hours + access_token_expires_in nil # Assign a custom TTL for implicit grants. # custom_access_token_expires_in do |oauth_client| diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index 7d2d215d..ad83dc78 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -3,7 +3,7 @@ en: attributes: doorkeeper/application: name: 'Name' - redirect_uri: 'Redirect URI' + redirect_uri: 'Redirect URIs' errors: models: doorkeeper/application: @@ -19,8 +19,9 @@ en: confirmations: destroy: 'Are you sure?' buttons: + back: 'Back' edit: 'Edit' - destroy: 'Destroy' + destroy: 'remove' submit: 'Submit' cancel: 'Cancel' authorize: 'Authorize' @@ -33,26 +34,24 @@ en: edit: title: 'Edit application' index: - title: 'Your applications' - new: 'New Application' + title: 'Registered Apps' + new: 'New App' name: 'Name' - callback_url: 'Callback URL' + callback_url: 'Redirect URIs' new: - title: 'New Application' + title: 'New App' show: - title: 'Application: %{name}' - application_id: 'Application Id' - secret: 'Secret' - scopes: 'Scopes' - callback_urls: 'Callback urls' - actions: 'Actions' + title: '%{name}' + application_id: 'App ID' + secret: 'App Secret' + callback_urls: 'Redirect URIs' authorizations: buttons: authorize: 'Authorize' deny: 'Deny' error: - title: 'An error has occurred' + title: 'Invalid Authorization Request' new: title: 'Authorization required' prompt: 'Authorize %{client_name} to use your account?' @@ -66,10 +65,10 @@ en: buttons: revoke: 'Revoke' index: - title: 'Your authorized applications' - application: 'Application' - created_at: 'Created At' - date_format: '%Y-%m-%d %H:%M:%S' + title: 'Authorized Apps' + application: 'App' + created_at: 'Date Authorized' + date_format: '%Y-%m-%d' errors: messages: @@ -117,7 +116,7 @@ en: layouts: admin: nav: - oauth2_provider: 'OAuth2 Provider' + app: 'METAMAPS' applications: 'Applications' application: title: 'OAuth authorization required' diff --git a/public/famous/main.js b/public/famous/main.js index c14f5c67..2a147e83 100644 --- a/public/famous/main.js +++ b/public/famous/main.js @@ -296,6 +296,9 @@ Metamaps.Famous.build = function () { { duration: 300, curve: 'easeIn' } ); }; + f.explore.setApps = function (section) { + f.explore.surf.setContent(templates[section + 'AppsContent']); + }; f.explore.set = function (section, mapperId) { var loggedIn = Metamaps.Active.Mapper ? 'Auth' : ''; @@ -402,4 +405,4 @@ Metamaps.Famous.build = function () { f.logo.show(); }// build -}); \ No newline at end of file +}); diff --git a/public/famous/templates.js b/public/famous/templates.js index 01aee3ca..66e6b9ac 100644 --- a/public/famous/templates.js +++ b/public/famous/templates.js @@ -30,5 +30,12 @@ t.logoContent += ''; t.featuredAuthContent += '
    Recently Active
    '; t.featuredAuthContent += '
    Featured
    '; +/* apps bars */ + t.registeredAppsContent = '
    Registered Apps
    '; + t.registeredAppsContent += '
    Authorized Apps
    '; + + t.authorizedAppsContent = '
    Registered Apps
    '; + t.authorizedAppsContent += '
    Authorized Apps
    '; + module.exports = t; }); From 34d3a80db17a33ac2df02a4c274fa2e5fce51559 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 25 Mar 2016 10:05:22 +0800 Subject: [PATCH 281/305] use new pundit-enabled snorlax --- Gemfile | 3 +-- Gemfile.lock | 18 ++++++++---------- app/controllers/api/restful_controller.rb | 1 - 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/Gemfile b/Gemfile index bf5997af..eff6826f 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,6 @@ gem 'devise' gem 'redis' gem 'pg' gem 'pundit' -gem 'cancan' gem 'pundit_extra' gem 'formula' gem 'formtastic' @@ -17,7 +16,7 @@ gem 'best_in_place' #in-place editing gem 'kaminari' # pagination gem 'uservoice-ruby' gem 'dotenv' -gem 'snorlax', '~> 0.1.3' +gem 'snorlax' gem 'httparty' gem 'sequenced', '~> 2.0.0' gem 'active_model_serializers', '~> 0.8.1' diff --git a/Gemfile.lock b/Gemfile.lock index ff5c1317..6f4948ad 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -57,8 +57,6 @@ GEM debug_inspector (>= 0.0.1) builder (3.2.2) byebug (8.2.2) - cancan (1.6.10) - cancancan (1.10.1) climate_control (0.0.3) activesupport (>= 3.0) cocaine (0.5.8) @@ -123,10 +121,12 @@ GEM activesupport (>= 3.0.0) loofah (2.0.3) nokogiri (>= 1.5.9) - mail (2.6.3) - mime-types (>= 1.16, < 3) + mail (2.6.4) + mime-types (>= 1.16, < 4) method_source (0.8.2) - mime-types (2.99.1) + mime-types (3.0) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0221) mimemagic (0.3.0) mini_portile2 (2.0.0) minitest (5.8.4) @@ -191,7 +191,7 @@ GEM activesupport (= 4.2.4) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (11.1.0) + rake (11.1.1) redis (3.2.2) responders (2.1.1) railties (>= 4.2.0, < 5.1) @@ -230,8 +230,7 @@ GEM simplecov-html (~> 0.10.0) simplecov-html (0.10.0) slop (3.6.0) - snorlax (0.1.4) - cancancan (~> 1.10.1) + snorlax (0.1.5) rails (> 4.1) sprockets (3.5.2) concurrent-ruby (~> 1.0) @@ -265,7 +264,6 @@ DEPENDENCIES best_in_place better_errors binding_of_caller - cancan coffee-rails delayed_job (~> 4.0.2) delayed_job_active_record (~> 4.0.1) @@ -297,7 +295,7 @@ DEPENDENCIES sequenced (~> 2.0.0) shoulda-matchers simplecov - snorlax (~> 0.1.3) + snorlax tunemygc uglifier uservoice-ruby diff --git a/app/controllers/api/restful_controller.rb b/app/controllers/api/restful_controller.rb index 06396e3d..5a603fa8 100644 --- a/app/controllers/api/restful_controller.rb +++ b/app/controllers/api/restful_controller.rb @@ -4,7 +4,6 @@ class API::RestfulController < ActionController::Base snorlax_used_rest! - rescue_from(Pundit::NotAuthorizedError) { |e| respond_with_standard_error e, 403 } load_and_authorize_resource only: [:show, :update, :destroy] def create From 530a16cadb74505e7c318a9bf106330fe776d85b Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 25 Mar 2016 10:26:32 +0800 Subject: [PATCH 282/305] instantiate_resource --- app/controllers/api/restful_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/restful_controller.rb b/app/controllers/api/restful_controller.rb index 06396e3d..b0d793d1 100644 --- a/app/controllers/api/restful_controller.rb +++ b/app/controllers/api/restful_controller.rb @@ -9,7 +9,7 @@ class API::RestfulController < ActionController::Base def create authorize resource_class - instantiate_resouce + instantiate_resource resource.user = current_user create_action respond_with_resource From 415c9b8ac38677ba2149e2d2b0fad670938b0d60 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Thu, 24 Mar 2016 21:26:07 -0700 Subject: [PATCH 283/305] final touchups on oauth --- app/controllers/application_controller.rb | 10 +- app/helpers/application_helper.rb | 10 +- app/views/layouts/_foot.html.erb | 32 ++++++ app/views/layouts/_head.html.erb | 71 ++++++++++++++ app/views/layouts/_lightboxes.html.erb | 61 ++++++------ app/views/layouts/application.html.erb | 114 +--------------------- app/views/layouts/doorkeeper.html.erb | 109 +++------------------ 7 files changed, 160 insertions(+), 247 deletions(-) create mode 100644 app/views/layouts/_foot.html.erb create mode 100644 app/views/layouts/_head.html.erb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6e67ba25..0568813b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,12 +1,12 @@ class ApplicationController < ActionController::Base + include ApplicationHelper include Pundit include PunditExtra rescue_from Pundit::NotAuthorizedError, with: :handle_unauthorized protect_from_forgery - before_action :get_invite_link after_action :allow_embedding - + def default_serializer_options { root: false } end @@ -33,7 +33,7 @@ class ApplicationController < ActionController::Base def handle_unauthorized head :forbidden # TODO make this better end - + private def require_no_user @@ -69,10 +69,6 @@ private authenticated? && current_user.admin end - def get_invite_link - @invite_link = "#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : "") - end - def allow_embedding #allow all response.headers.except! 'X-Frame-Options' diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3db990db..38e82922 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,12 +1,12 @@ -module ApplicationHelper +module ApplicationHelper def get_metacodeset - @m = user.settings.metacodes + @m = current_user.settings.metacodes set = @m[0].include?("metacodeset") ? MetacodeSet.find(@m[0].sub("metacodeset-","").to_i) : false return set end def user_metacodes - @m = user.settings.metacodes + @m = current_user.settings.metacodes set = get_metacodeset if set @metacodes = set.metacodes.to_a @@ -15,4 +15,8 @@ module ApplicationHelper end @metacodes.sort! {|m1,m2| m2.name.downcase <=> m1.name.downcase }.rotate!(-1) end + + def determine_invite_link + "#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : "") + end end diff --git a/app/views/layouts/_foot.html.erb b/app/views/layouts/_foot.html.erb new file mode 100644 index 00000000..6900fa23 --- /dev/null +++ b/app/views/layouts/_foot.html.erb @@ -0,0 +1,32 @@ +<%= render :partial => 'layouts/lightboxes' %> +<%= render :partial => 'layouts/templates' %> +<%= render :partial => 'shared/metacodeBgColors' %> + + +<%= render :partial => 'layouts/googleanalytics' if Rails.env.production? %> + + diff --git a/app/views/layouts/_head.html.erb b/app/views/layouts/_head.html.erb new file mode 100644 index 00000000..07881bce --- /dev/null +++ b/app/views/layouts/_head.html.erb @@ -0,0 +1,71 @@ + + + + + + <%=h yield(:title) %> + <%= csrf_meta_tags %> + + + <%= stylesheet_link_tag "application", :media => "all" %> + <%= javascript_include_tag "application" %> + + + + + + + + + + diff --git a/app/views/layouts/_lightboxes.html.erb b/app/views/layouts/_lightboxes.html.erb index 7487adad..603c99a3 100644 --- a/app/views/layouts/_lightboxes.html.erb +++ b/app/views/layouts/_lightboxes.html.erb @@ -6,7 +6,7 @@ +
    +
    +
    + -

    Metamaps.cc is a free and open source web platform that supports real-time sense-making and distributed collaboration between individuals, communities and organizations.

    Using an intuitive graph-based interface, Metamaps.cc helps map out networks of people, ideas, resources, stories, experiences, conversations and much more. The platform is evolving for a range of applications amidst a growing network of designers, developers, facilitators, practitioners, entrepreneurs, and artists.

    Metamaps.cc is created and maintained by a distributed community of contributors passionate about the evolution of collaboration, alternative forms of value creation and increase of collective intelligence through the lens of the open culture and the peer-to-peer revolution.

    - +
    @@ -158,8 +158,8 @@ + <% if current_user %>

    SHARE INVITE

    - -
    + +

    The Metamaps platform is currently in an invite-only beta with the express purpose of creating a high value knowledge ecosystem, a diverse community of contributors and a culture of collaboration and curiosity.

    As a valued beta tester, you have the ability to invite your peers, colleagues and collaborators onto the platform.

    Below is a personal invite link containing your unique access code, which can be used multiple times.

    -

    <%= @invite_link %> +

    <%= determine_invite_link %>

    - +
    - + <% # this is the create new map form %> -
    +
    <%= render :partial => 'layouts/newmap' %>
    - -
    + +
    <%= render :partial => 'shared/forkmap' %>
    @@ -250,9 +250,8 @@ <%= render :partial => 'shared/switchmetacodes' %>
    <% end %> - +
    - diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 5774708e..2b8aa96e 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -5,77 +5,7 @@ # displayed within, based on URL #%> - - - - - - <%=h yield(:title) %> - <%= csrf_meta_tags %> - - - <%= stylesheet_link_tag "application", :media => "all" %> - <%= javascript_include_tag "application" %> - - - - - - - - - - +<%= render :partial => 'layouts/head' %> "> @@ -90,7 +20,7 @@ <% classes = action_name == "home" ? "homePage" : "" classes += action_name == "home" && authenticated? ? " explorePage" : "" classes += controller_name == "maps" && action_name == "index" ? " explorePage" : "" - if controller_name == "maps" && action_name == "show" + if controller_name == "maps" && action_name == "show" classes += " mapPage" if policy(@map).update? classes += " canEditMap" @@ -103,7 +33,7 @@ %>
    - + <%= render :partial => 'layouts/upperelements', :locals => { :appsPage => false } %> <%= yield %> @@ -124,40 +54,4 @@ <% end %> -<%= render :partial => 'layouts/lightboxes' %> -<%= render :partial => 'layouts/templates' %> -<%= render :partial => 'shared/metacodeBgColors' %> - - -<%= render :partial => 'layouts/googleanalytics' if Rails.env.production? %> - - +<%= render :partial => 'layouts/foot' %> diff --git a/app/views/layouts/doorkeeper.html.erb b/app/views/layouts/doorkeeper.html.erb index 76caf7a5..f6ae4e91 100644 --- a/app/views/layouts/doorkeeper.html.erb +++ b/app/views/layouts/doorkeeper.html.erb @@ -5,77 +5,7 @@ # displayed within, based on URL #%> - - - - - - <%=h yield(:title) %> - <%= csrf_meta_tags %> - - - <%= stylesheet_link_tag "application", :media => "all" %> - <%= javascript_include_tag "application" %> - - - - - - - - - - +<%= render :partial => 'layouts/head' %> @@ -88,38 +18,25 @@ <%= content_tag :div, class: "main" do %>
    - + <%= render :partial => 'layouts/upperelements', :locals => {:appsPage => true } %> <%= yield %> +
    + <% if current_user %> + <% # for creating and pulling in topics and synapses %> + <%= render :partial => 'maps/newtopic' %> + <%= render :partial => 'maps/newsynapse' %> + <% # for populating the change metacode list on the topic card %> + <%= render :partial => 'shared/metacodeoptions' %> + <% end %> + <%= render :partial => 'layouts/lowermapelements' %> +
    <% end %> -<%= render :partial => 'layouts/lightboxes' %> -<%= render :partial => 'layouts/templates' %> -<%= render :partial => 'shared/metacodeBgColors' %> - - -<%= render :partial => 'layouts/googleanalytics' if Rails.env.production? %> - - +<%= render :partial => 'layouts/foot' %> From c6f1e3cc4a927a5f0bd6aabace0610125d6a4b8a Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Thu, 24 Mar 2016 23:29:08 -0700 Subject: [PATCH 284/305] breaking the mapping_policy --- app/controllers/api/restful_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/restful_controller.rb b/app/controllers/api/restful_controller.rb index 77edd69f..8ecad963 100644 --- a/app/controllers/api/restful_controller.rb +++ b/app/controllers/api/restful_controller.rb @@ -7,9 +7,9 @@ class API::RestfulController < ActionController::Base load_and_authorize_resource only: [:show, :update, :destroy] def create - authorize resource_class instantiate_resource resource.user = current_user + authorize resource_class create_action respond_with_resource end From b36d5df6cb3c61fa22d9b4dd35119f7570ba27ac Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Thu, 24 Mar 2016 23:33:26 -0700 Subject: [PATCH 285/305] this may make it work --- app/controllers/api/restful_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/restful_controller.rb b/app/controllers/api/restful_controller.rb index 8ecad963..2dd00d4d 100644 --- a/app/controllers/api/restful_controller.rb +++ b/app/controllers/api/restful_controller.rb @@ -9,7 +9,7 @@ class API::RestfulController < ActionController::Base def create instantiate_resource resource.user = current_user - authorize resource_class + authorize resource create_action respond_with_resource end From 87d6dfe8de0c2687eac2e2cb1717338fc5bb5791 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Thu, 24 Mar 2016 23:36:57 -0700 Subject: [PATCH 286/305] fix the autocomplete --- app/helpers/topics_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index 482a663c..9e7d82dc 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -8,7 +8,7 @@ module TopicsHelper topic['id'] = t.id topic['label'] = t.name topic['value'] = t.name - topic['description'] = t.desc.truncate(70) # make this return matched results + topic['description'] = t.desc ? t.desc.truncate(70) : '' # make this return matched results topic['type'] = t.metacode.name topic['typeImageURL'] = t.metacode.icon topic['permission'] = t.permission From 6c055ea3b90a2a6af08735dda618e5ba111420ae Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 25 Mar 2016 16:35:43 +0800 Subject: [PATCH 287/305] add missing synapses_csv function (fixes #504) --- app/models/topic.rb | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/app/models/topic.rb b/app/models/topic.rb index dfbe7014..f1a73c1b 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -81,6 +81,38 @@ class Topic < ActiveRecord::Base super(:methods =>[:user_name, :user_image, :map_count, :synapse_count, :inmaps, :inmapsLinks]) end + # TODO move to a decorator? + def synapses_csv(output_format = 'array') + output = [] + synapses.each do |synapse| + if synapse.category == 'from-to' + if synapse.node1_id == id + output << synapse.node1_id.to_s + '->' + synapse.node2_id.to_s + elsif synapse.node2_id == id + output << synapse.node2_id.to_s + '<-' + synapse.node1_id.to_s + else + fail 'invalid synapse on topic in synapse_csv' + end + elsif synapse.category == 'both' + if synapse.node1_id == id + output << synapse.node1_id.to_s + '<->' + synapse.node2_id.to_s + elsif synapse.node2_id == id + output << synapse.node2_id.to_s + '<->' + synapse.node1_id.to_s + else + fail 'invalid synapse on topic in synapse_csv' + end + end + end + if output_format == 'array' + return output + elsif output_format == 'text' + return output.join('; ') + else + fail 'invalid argument to synapses_csv' + end + output + end + def topic_autocomplete_method "Get: #{self.name}" end From 6df7fa849a37d1e12c49f64908587e85fc5f3755 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 7 Feb 2016 16:56:03 +0800 Subject: [PATCH 288/305] bare minimum topic import functionality - use by Ctrl+V onto the map canvas itself --- app/assets/javascripts/application.js | 1 + .../javascripts/src/Metamaps.Import.js.erb | 104 ++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 app/assets/javascripts/src/Metamaps.Import.js.erb diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index b3b9cdfe..2fd7ab6a 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -25,6 +25,7 @@ //= require ./src/views/room //= require ./src/JIT //= require ./src/Metamaps +//= require ./src/Metamaps.Import //= require ./src/Metamaps.JIT //= require_directory ./shims //= require_directory ./require diff --git a/app/assets/javascripts/src/Metamaps.Import.js.erb b/app/assets/javascripts/src/Metamaps.Import.js.erb new file mode 100644 index 00000000..d17077ae --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Import.js.erb @@ -0,0 +1,104 @@ +/* + * Example tab-separated input: + * Some fields will be ignored + * + * id name metacode desc link user.name permission synapses + * 1 topic1 Catalyst admin commons 1->7 + * 2 topic2 Event admin commons + * 5 topic Action admin commons + * 6 topic6 Action admin commons 6->7 + * 7 topic7 Action admin commons 7->8 7<-6 7<-1 + * 8 topic8 Action admin commons 8<-7 + */ + +Metamaps.Import = { + headersWhitelist: [ + 'name', 'metacode', 'desc', 'link', 'permission' + ], + + init: function() { + var self = Metamaps.Import; + $('body').bind('paste', function(e) { + var text = e.originalEvent.clipboardData.getData('text/plain'); + var parsed = self.parseTabbedString(text); + + if (confirm("Are you sure you want to create " + parsed.length + " new topics?")) { + self.importTopics(parsed); + }//if + }); + }, + + importTopics: function(parsedTopics) { + var self = Metamaps.Import; + + var x = -200; + var y = -200; + parsedTopics.forEach(function(topic) { + self.createTopicWithParameters( + topic.name, topic.metacode, topic.permission, + topic.desc, topic.link, x, y + ); + + // update positions of topics + x += 50; + if (x > 200) { + y += 50; + x = -200; + }//if + }); + }, + + parseTabbedString: function(text) { + var self = Metamaps.Import; + + // determine line ending and split lines + var delim = "\n"; + if (text.indexOf("\r\n") !== -1) { + delim = "\r\n"; + }//if + var lines = text.split(delim); + + // get csv-style headers to name the object fields + var headers = lines[0].split(' '); //tab character + + var results = []; + lines.forEach(function(line, index) { + if (index === 0) return; + if (line == "") return; + + var topic = {}; + line.split(" ").forEach(function(field, index) { + if (self.headersWhitelist.indexOf(headers[index]) === -1) return; + topic[headers[index]] = field; + }); + results.push(topic); + }); + return results; + }, + + createTopicWithParameters: function(name, metacode_name, permission, desc, link, xloc, yloc) { + var self = Metamaps.Topic; + + var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null; + if (metacode === null) return console.error("metacode not found"); + + var topic = new Metamaps.Backbone.Topic({ + name: name, + metacode_id: metacode.id, + permission: permission || Metamaps.Active.Map.get('permission'), + desc: desc, + link: link + }); + Metamaps.Topics.add(topic); + + var mapping = new Metamaps.Backbone.Mapping({ + xloc: xloc, + yloc: yloc, + mappable_id: topic.cid, + mappable_type: "Topic", + }); + Metamaps.Mappings.add(mapping); + + self.renderTopic(mapping, topic, true, true); // this function also includes the creation of the topic in the database + }, +}; From b47ed7b5b44f3498c04bdfa4952c2819d35d35b9 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 7 Feb 2016 17:51:01 +0800 Subject: [PATCH 289/305] don't ask about adding 0 topics --- app/assets/javascripts/src/Metamaps.Import.js.erb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Import.js.erb b/app/assets/javascripts/src/Metamaps.Import.js.erb index d17077ae..14612247 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js.erb +++ b/app/assets/javascripts/src/Metamaps.Import.js.erb @@ -22,7 +22,9 @@ Metamaps.Import = { var text = e.originalEvent.clipboardData.getData('text/plain'); var parsed = self.parseTabbedString(text); - if (confirm("Are you sure you want to create " + parsed.length + " new topics?")) { + if (parsed.length > 0 && + confirm("Are you sure you want to create " + parsed.length + + " new topics?")) { self.importTopics(parsed); }//if }); @@ -76,7 +78,8 @@ Metamaps.Import = { return results; }, - createTopicWithParameters: function(name, metacode_name, permission, desc, link, xloc, yloc) { + createTopicWithParameters: function(name, metacode_name, permission, desc, + link, xloc, yloc) { var self = Metamaps.Topic; var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null; @@ -99,6 +102,7 @@ Metamaps.Import = { }); Metamaps.Mappings.add(mapping); - self.renderTopic(mapping, topic, true, true); // this function also includes the creation of the topic in the database + // this function also includes the creation of the topic in the database + self.renderTopic(mapping, topic, true, true); }, }; From 0c1e12a301a2daea74f346a786f192c60b22ac75 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 14 Feb 2016 22:39:29 +0800 Subject: [PATCH 290/305] use state machine to implement smarter topic/synapse import also include better auto-layout of new topics if x/y not specified --- .../javascripts/src/Metamaps.Import.js.erb | 237 ++++++++++++++---- 1 file changed, 194 insertions(+), 43 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Import.js.erb b/app/assets/javascripts/src/Metamaps.Import.js.erb index 14612247..b604e970 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js.erb +++ b/app/assets/javascripts/src/Metamaps.Import.js.erb @@ -12,42 +12,34 @@ */ Metamaps.Import = { - headersWhitelist: [ - 'name', 'metacode', 'desc', 'link', 'permission' + topicWhitelist: [ + 'name', 'metacode', 'description', 'link', 'permission' ], + synapseWhitelist: [ + 'desc', 'description', 'category', 'topic1', 'topic2', 'permission' + ], init: function() { var self = Metamaps.Import; $('body').bind('paste', function(e) { var text = e.originalEvent.clipboardData.getData('text/plain'); - var parsed = self.parseTabbedString(text); - if (parsed.length > 0 && - confirm("Are you sure you want to create " + parsed.length + - " new topics?")) { - self.importTopics(parsed); + var results = self.parseTabbedString(text); + var topics = results.topics; + var synapses = results.synapses; + + if (topics.length > 0 || synapses.length > 0) { + if (confirm("Are you sure you want to create " + topics.length + + " new topics and " + synapses.length + " new synapses?")) { + self.importTopics(topics); + self.importSynapses(synapses); + }//if }//if }); }, - importTopics: function(parsedTopics) { - var self = Metamaps.Import; - - var x = -200; - var y = -200; - parsedTopics.forEach(function(topic) { - self.createTopicWithParameters( - topic.name, topic.metacode, topic.permission, - topic.desc, topic.link, x, y - ); - - // update positions of topics - x += 50; - if (x > 200) { - y += 50; - x = -200; - }//if - }); + abort: function(message) { + console.error(message); }, parseTabbedString: function(text) { @@ -58,30 +50,162 @@ Metamaps.Import = { if (text.indexOf("\r\n") !== -1) { delim = "\r\n"; }//if + + var STATES = { + UNKNOWN: 0, + TOPICS_NEED_HEADERS: 1, + SYNAPSES_NEED_HEADERS: 2, + TOPICS: 3, + SYNAPSES: 4, + }; + + // state & lines determine parser behaviour + var state = STATES.UNKNOWN; var lines = text.split(delim); + var results = { topics: [], synapses: [] } + var topicHeaders = []; + var synapseHeaders = []; - // get csv-style headers to name the object fields - var headers = lines[0].split(' '); //tab character - - var results = []; - lines.forEach(function(line, index) { - if (index === 0) return; - if (line == "") return; - - var topic = {}; - line.split(" ").forEach(function(field, index) { - if (self.headersWhitelist.indexOf(headers[index]) === -1) return; - topic[headers[index]] = field; + lines.forEach(function(line_raw, index) { + var line = line_raw.split(' '); // tab character + var noblanks = line.filter(function(elt) { + return elt !== ""; }); - results.push(topic); + switch(state) { + case STATES.UNKNOWN: + if (noblanks.length === 0) { + state = STATES.UNKNOWN; + break; + } else if (noblanks.length === 1 && line[0].toLowerCase() === 'topics') { + state = STATES.TOPICS_NEED_HEADERS; + break; + } else if (noblanks.length === 1 && line[0].toLowerCase() === 'synapses') { + state = STATES.SYNAPSES_NEED_HEADERS; + break; + } + state = STATES.TOPICS_NEED_HEADERS; + // FALL THROUGH - if we're not sure what to do, pretend + // we're on the TOPICS_NEED_HEADERS state and parse some headers + + case STATES.TOPICS_NEED_HEADERS: + if (noblanks.length < 2) { + return self.abort("Not enough topic headers on line " + index); + } + topicHeaders = line.map(function(header, index) { + return header.toLowerCase().replace('description', 'desc'); + }); + state = STATES.TOPICS; + break; + + case STATES.SYNAPSES_NEED_HEADERS: + if (noblanks.length < 2) { + return self.abort("Not enough synapse headers on line " + index); + } + synapseHeaders = line.map(function(header, index) { + return header.toLowerCase().replace('description', 'desc'); + }); + state = STATES.SYNAPSES; + break; + + case STATES.TOPICS: + if (noblanks.length === 0) { + state = STATES.UNKNOWN; + } else if (noblanks.length === 1 && line[0].toLowerCase() === 'topics') { + state = STATES.TOPICS_NEED_HEADERS; + } else if (noblanks.length === 1 && line[0].toLowerCase() === 'synapses') { + state = STATES.SYNAPSES_NEED_HEADERS; + } else { + var topic = {}; + line.forEach(function(field, index) { + var header = topicHeaders[index]; + if (self.topicWhitelist.indexOf(header) === -1) return; + topic[header] = field; + if (header === 'x' || header === 'y') { + topic[header] = parseInt(topic[header]); + }//if + }); + results.topics.push(topic); + } + break; + + case STATES.SYNAPSES: + if (noblanks.length === 0) { + state = STATES.UNKNOWN; + } else if (noblanks.length === 1 && line[0].toLowerCase() === 'topics') { + state = STATES.TOPICS_NEED_HEADERS; + } else if (noblanks.length === 1 && line[0].toLowerCase() === 'synapses') { + state = STATES.SYNAPSES_NEED_HEADERS; + } else { + var synapse = {}; + line.forEach(function(field, index) { + var header = synapseHeaders[index]; + if (self.synapseWhitelist.indexOf(header) === -1) return; + synapse[header] = field; + if (header === 'topic1' || header === 'topic2') { + synapse[header] = parseInt(header); + }//if + }); + results.synapses.push(synapse); + } + break; + + default: + return self.abort("Invalid state while parsing import data. " + + "Check code."); + } }); + return results; }, - createTopicWithParameters: function(name, metacode_name, permission, desc, - link, xloc, yloc) { - var self = Metamaps.Topic; + importTopics: function(parsedTopics) { + var self = Metamaps.Import; + + // up to 25 topics: scale 100 + // up to 81 topics: scale 200 + // up to 169 topics: scale 300 + var scale = Math.floor((Math.sqrt(parsedTopics.length) - 1) / 4) * 100; + if (scale < 100) scale = 100; + var autoX = -scale; + var autoY = -scale; + + parsedTopics.forEach(function(topic) { + var x, y; + if (topic.x && topic.y) { + x = topic.x; + y = topic.y; + } else { + x = autoX; + y = autoY; + autoX += 50; + if (autoX > scale) { + autoY += 50; + autoX = -scale; + } + } + + self.createTopicWithParameters( + topic.name, topic.metacode, topic.permission, + topic.desc, topic.link, x, y, topic.id + ); + }); + }, + + importSynapses: function(parsedSynapses) { + var self = Metamaps.Import; + + parsedSynapses.forEach(function(synapse) { + self.createSynapseWithParameters( + synapse.desc, synapse.category, synapse.permission, + synapse.topic1, synapse.topic2 + ); + }); + }, + + createTopicWithParameters: function(name, metacode_name, permission, desc, + link, xloc, yloc, import_id) { + $(document).trigger(Metamaps.Map.events.editedByActiveMapper); var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null; if (metacode === null) return console.error("metacode not found"); @@ -90,7 +214,8 @@ Metamaps.Import = { metacode_id: metacode.id, permission: permission || Metamaps.Active.Map.get('permission'), desc: desc, - link: link + link: link, + import_id: import_id }); Metamaps.Topics.add(topic); @@ -103,6 +228,32 @@ Metamaps.Import = { Metamaps.Mappings.add(mapping); // this function also includes the creation of the topic in the database - self.renderTopic(mapping, topic, true, true); + Metamaps.Topic.renderTopic(mapping, topic, true, true); + + Metamaps.Famous.viz.hideInstructions(); + }, + + createSynapseWithParameters: function(description, category, permission, + node1_id, node2_id) { + var topic1 = Metamaps.Topics.where({import_id: node1_id}); + var topic2 = Metamaps.Topics.where({import_id: node2_id}); + var node1 = topic1.get('node'); + var node2 = topic2.get('node'); + // TODO check if topic1 and topic2 were sucessfully found... + + var synapse = new Metamaps.Backbone.Synapse({ + desc: description, + category: category, + permission: permission, + node1_id: node1_id, + node2_id: node2_id, + }); + + var mapping = new Metamaps.Backbone.Mapping({ + mappable_type: "Synapse", + mappable_id: synapse.cid, + }); + + Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, true); }, }; From 61262aaec2cb4d4979918a375bd9bbc43c56b825 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 5 Feb 2016 17:49:59 +0800 Subject: [PATCH 291/305] implement csv/xls export --- app/models/map.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/map.rb b/app/models/map.rb index 5cd30bbe..696982da 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -101,7 +101,7 @@ class Map < ActiveRecord::Base end end end - + def decode_base64(imgBase64) decoded_data = Base64.decode64(imgBase64) From 8f532708ce32626d1d5a309f7568efed5ade968f Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 12 Feb 2016 15:57:34 +0800 Subject: [PATCH 292/305] update xls/csv format to better serialize topics and synapses --- app/models/map.rb | 23 +++++++++++++++++++++-- app/views/maps/show.xls.erb | 35 ++++++++++++++++++++++++++++++----- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/app/models/map.rb b/app/models/map.rb index 696982da..e786eb91 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -86,12 +86,17 @@ class Map < ActiveRecord::Base def to_csv(options = {}) CSV.generate(options) do |csv| - csv << ["id", "name", "metacode", "desc", "link", "user.name", "permission", "synapses"] - self.topics.each do |topic| + csv << ["topics"] + csv << ["id", "name", "metacode", "x", "y", "desc", "link", "user.name", "permission"] + self.topicmappings.each do |mapping| + topic = mapping.mappable + next if topic.nil? csv << [ topic.id, topic.name, topic.metacode.name, + mapping.x, + mapping.y, topic.desc, topic.link, topic.user.name, @@ -99,6 +104,20 @@ class Map < ActiveRecord::Base topic.synapses_csv("text") ] end + csv << [] + csv << ["synapses"] + csv << ["id", "description", "category", "topic1", "topic2", "username", "permission"] + self.synapses.each do |synapse| + csv << [ + synapse.id, + synapse.desc, + synapse.category, + synapse.node1_id, + synapse.node2_id, + synapse.user.name, + synapse.permission + ] + end end end diff --git a/app/views/maps/show.xls.erb b/app/views/maps/show.xls.erb index d00dd36e..4b22257e 100644 --- a/app/views/maps/show.xls.erb +++ b/app/views/maps/show.xls.erb @@ -1,26 +1,51 @@ + + + - - <% @map.topics.each do |topic| %> + <% @map.topicmappings.each do |mapping| %> + <% topic = mapping.mappable %> + <% next if topic.nil? %> + + - <% topic.synapses_csv.each do |s_text| %> - - <% end %> + + <% end %> + + + + + + + + + + + + <% @map.synapses.each do |synapse| %> + + + + + + + + <% end %>
    Topics
    ID Name MetacodeXY Description Link Username PermissionSynapses
    <%= topic.id %> <%= topic.name %> <%= topic.metacode.name %><%= mapping.xloc %><%= mapping.yloc %> <%= topic.desc %> <%= topic.link %> <%= topic.user.name %> <%= topic.permission %><%= s_text %>
    Synapses
    IDDescriptionCategoryTopic1Topic2UsernamePermission
    <%= synapse.id %><%= synapse.desc %><%= synapse.category %><%= synapse.node1_id %><%= synapse.node2_id %><%= synapse.user.name %><%= synapse.permission %>
    From ea677f8a6b36993674d94ff45b16b24b4688ea73 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 13 Feb 2016 12:53:07 +0800 Subject: [PATCH 293/305] DRY up csv/xls rendering, put it into model --- app/models/map.rb | 69 ++++++++++++++++++++----------------- app/views/maps/show.xls.erb | 54 ++++------------------------- 2 files changed, 44 insertions(+), 79 deletions(-) diff --git a/app/models/map.rb b/app/models/map.rb index e786eb91..9efc278b 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -84,39 +84,46 @@ class Map < ActiveRecord::Base json end + def to_spreadsheet + spreadsheet = [] + spreadsheet << ["Topics"] + spreadsheet << ["Id", "Name", "Metacode", "X", "Y", "Description", "Link", "User", "Permission"] + self.topicmappings.each do |mapping| + topic = mapping.mappable + next if topic.nil? + spreadsheet << [ + topic.id, + topic.name, + topic.metacode.name, + mapping.xloc, + mapping.yloc, + topic.desc, + topic.link, + topic.user.name, + topic.permission + ] + end + spreadsheet << [] + spreadsheet << ["Synapses"] + spreadsheet << ["Id", "Description", "Category", "Topic1", "Topic2", "User", "Permission"] + self.synapses.each do |synapse| + spreadsheet << [ + synapse.id, + synapse.desc, + synapse.category, + synapse.node1_id, + synapse.node2_id, + synapse.user.name, + synapse.permission + ] + end + spreadsheet + end + def to_csv(options = {}) CSV.generate(options) do |csv| - csv << ["topics"] - csv << ["id", "name", "metacode", "x", "y", "desc", "link", "user.name", "permission"] - self.topicmappings.each do |mapping| - topic = mapping.mappable - next if topic.nil? - csv << [ - topic.id, - topic.name, - topic.metacode.name, - mapping.x, - mapping.y, - topic.desc, - topic.link, - topic.user.name, - topic.permission, - topic.synapses_csv("text") - ] - end - csv << [] - csv << ["synapses"] - csv << ["id", "description", "category", "topic1", "topic2", "username", "permission"] - self.synapses.each do |synapse| - csv << [ - synapse.id, - synapse.desc, - synapse.category, - synapse.node1_id, - synapse.node2_id, - synapse.user.name, - synapse.permission - ] + to_spreadsheet.each do |line| + csv << line end end end diff --git a/app/views/maps/show.xls.erb b/app/views/maps/show.xls.erb index 4b22257e..2f11a946 100644 --- a/app/views/maps/show.xls.erb +++ b/app/views/maps/show.xls.erb @@ -1,51 +1,9 @@ - - - - - - - - - - - - - <% @map.topicmappings.each do |mapping| %> - <% topic = mapping.mappable %> - <% next if topic.nil? %> - - - - - - - - - - - - <% end %> - - - - - - - - - - - - <% @map.synapses.each do |synapse| %> - - - - - - - - - + <% @map.to_spreadsheet.each do |line| %> + + <% line.each do |field| %> + + <% end %> + <% end %>
    Topics
    IDNameMetacodeXYDescriptionLinkUsernamePermission
    <%= topic.id %><%= topic.name %><%= topic.metacode.name %><%= mapping.xloc %><%= mapping.yloc %><%= topic.desc %><%= topic.link %><%= topic.user.name %><%= topic.permission %>
    Synapses
    IDDescriptionCategoryTopic1Topic2UsernamePermission
    <%= synapse.id %><%= synapse.desc %><%= synapse.category %><%= synapse.node1_id %><%= synapse.node2_id %><%= synapse.user.name %><%= synapse.permission %>
    <%= field %>
    From c77cc32734f31cbfdede99a11859811cdff3773a Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 21 Feb 2016 16:28:31 +0800 Subject: [PATCH 294/305] import fixes - better abort logic & messaging - handle \r line delim - better example format at top --- .../javascripts/src/Metamaps.Import.js.erb | 64 ++++++++++++++----- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Import.js.erb b/app/assets/javascripts/src/Metamaps.Import.js.erb index b604e970..8a366560 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js.erb +++ b/app/assets/javascripts/src/Metamaps.Import.js.erb @@ -2,19 +2,30 @@ * Example tab-separated input: * Some fields will be ignored * - * id name metacode desc link user.name permission synapses - * 1 topic1 Catalyst admin commons 1->7 - * 2 topic2 Event admin commons - * 5 topic Action admin commons - * 6 topic6 Action admin commons 6->7 - * 7 topic7 Action admin commons 7->8 7<-6 7<-1 - * 8 topic8 Action admin commons 8<-7 + * Topics + * Id Name Metacode X Y Description Link User Permission + * 8 topic8 Action -231 131 admin commons + * 5 topic Action -229 -131 admin commons + * 7 topic7.1 Action -470 -55 hey admin commons + * 2 topic2 Event -57 -63 admin commons + * 1 topic1 Catalyst -51 50 admin commons + * 6 topic6 Action -425 63 admin commons + * + * Synapses + * Id Description Category Topic1 Topic2 User Permission + * 43 from-to 6 2 admin commons + * 44 from-to 6 1 admin commons + * 45 from-to 6 5 admin commons + * 46 from-to 2 7 admin commons + * 47 from-to 8 6 admin commons + * 48 from-to 8 1 admin commons + * */ Metamaps.Import = { topicWhitelist: [ 'name', 'metacode', 'description', 'link', 'permission' - ], + ], synapseWhitelist: [ 'desc', 'description', 'category', 'topic1', 'topic2', 'permission' ], @@ -25,6 +36,8 @@ Metamaps.Import = { var text = e.originalEvent.clipboardData.getData('text/plain'); var results = self.parseTabbedString(text); + if (results === false) return; + var topics = results.topics; var synapses = results.synapses; @@ -39,9 +52,16 @@ Metamaps.Import = { }, abort: function(message) { + alert("Sorry, something went wrong!\n\n" + message); console.error(message); }, + simplify: function(string) { + return string + .replace(/(^\s*|\s*$)/g, '') + .toLowerCase(); + }, + parseTabbedString: function(text) { var self = Metamaps.Import; @@ -49,9 +69,12 @@ Metamaps.Import = { var delim = "\n"; if (text.indexOf("\r\n") !== -1) { delim = "\r\n"; + } else if (text.indexOf("\r") !== -1) { + delim = "\r"; }//if var STATES = { + ABORT: -1, UNKNOWN: 0, TOPICS_NEED_HEADERS: 1, SYNAPSES_NEED_HEADERS: 2, @@ -67,7 +90,7 @@ Metamaps.Import = { var synapseHeaders = []; lines.forEach(function(line_raw, index) { - var line = line_raw.split(' '); // tab character + var line = line_raw.split("\t"); var noblanks = line.filter(function(elt) { return elt !== ""; }); @@ -76,10 +99,10 @@ Metamaps.Import = { if (noblanks.length === 0) { state = STATES.UNKNOWN; break; - } else if (noblanks.length === 1 && line[0].toLowerCase() === 'topics') { + } else if (noblanks.length === 1 && self.simplify(line[0]) === 'topics') { state = STATES.TOPICS_NEED_HEADERS; break; - } else if (noblanks.length === 1 && line[0].toLowerCase() === 'synapses') { + } else if (noblanks.length === 1 && self.simplify(line[0]) === 'synapses') { state = STATES.SYNAPSES_NEED_HEADERS; break; } @@ -89,7 +112,8 @@ Metamaps.Import = { case STATES.TOPICS_NEED_HEADERS: if (noblanks.length < 2) { - return self.abort("Not enough topic headers on line " + index); + self.abort("Not enough topic headers on line " + index); + state = STATES.ABORT; } topicHeaders = line.map(function(header, index) { return header.toLowerCase().replace('description', 'desc'); @@ -99,7 +123,8 @@ Metamaps.Import = { case STATES.SYNAPSES_NEED_HEADERS: if (noblanks.length < 2) { - return self.abort("Not enough synapse headers on line " + index); + self.abort("Not enough synapse headers on line " + index); + state = STATES.ABORT; } synapseHeaders = line.map(function(header, index) { return header.toLowerCase().replace('description', 'desc'); @@ -148,14 +173,19 @@ Metamaps.Import = { results.synapses.push(synapse); } break; - + case STATES.ABORT: + ; default: - return self.abort("Invalid state while parsing import data. " + - "Check code."); + self.abort("Invalid state while parsing import data. Check code."); + state = STATES.ABORT; } }); - return results; + if (state === STATES.ABORT) { + return false; + } else { + return results; + } }, From 387c863222df6d877bee7801ee1f40750f259a9f Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 21 Feb 2016 17:02:37 +0800 Subject: [PATCH 295/305] fix a bug with synapses and use cid to link new topics with synapses Synapses are now created client-side, but still rejected server-side --- .../javascripts/src/Metamaps.Import.js.erb | 26 +++++++++++-------- app/assets/javascripts/src/Metamaps.js.erb | 6 ++--- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Import.js.erb b/app/assets/javascripts/src/Metamaps.Import.js.erb index 8a366560..8d6b3867 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js.erb +++ b/app/assets/javascripts/src/Metamaps.Import.js.erb @@ -24,11 +24,12 @@ Metamaps.Import = { topicWhitelist: [ - 'name', 'metacode', 'description', 'link', 'permission' + 'id', 'name', 'metacode', 'description', 'link', 'permission' ], synapseWhitelist: [ - 'desc', 'description', 'category', 'topic1', 'topic2', 'permission' + 'id', 'desc', 'description', 'category', 'topic1', 'topic2', 'permission' ], + cidMappings: {}, //to be filled by import_id => cid mappings init: function() { var self = Metamaps.Import; @@ -145,7 +146,7 @@ Metamaps.Import = { var header = topicHeaders[index]; if (self.topicWhitelist.indexOf(header) === -1) return; topic[header] = field; - if (header === 'x' || header === 'y') { + if (['id', 'x', 'y'].indexOf(header) !== -1) { topic[header] = parseInt(topic[header]); }//if }); @@ -166,8 +167,8 @@ Metamaps.Import = { var header = synapseHeaders[index]; if (self.synapseWhitelist.indexOf(header) === -1) return; synapse[header] = field; - if (header === 'topic1' || header === 'topic2') { - synapse[header] = parseInt(header); + if (['id', 'topic1', 'topic2'].indexOf(header) !== -1) { + synapse[header] = parseInt(synapse[header]); }//if }); results.synapses.push(synapse); @@ -235,6 +236,7 @@ Metamaps.Import = { createTopicWithParameters: function(name, metacode_name, permission, desc, link, xloc, yloc, import_id) { + var self = Metamaps.Import; $(document).trigger(Metamaps.Map.events.editedByActiveMapper); var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null; if (metacode === null) return console.error("metacode not found"); @@ -245,9 +247,9 @@ Metamaps.Import = { permission: permission || Metamaps.Active.Map.get('permission'), desc: desc, link: link, - import_id: import_id }); Metamaps.Topics.add(topic); + self.cidMappings[import_id] = topic.cid; var mapping = new Metamaps.Backbone.Mapping({ xloc: xloc, @@ -260,13 +262,15 @@ Metamaps.Import = { // this function also includes the creation of the topic in the database Metamaps.Topic.renderTopic(mapping, topic, true, true); + Metamaps.Famous.viz.hideInstructions(); }, createSynapseWithParameters: function(description, category, permission, node1_id, node2_id) { - var topic1 = Metamaps.Topics.where({import_id: node1_id}); - var topic2 = Metamaps.Topics.where({import_id: node2_id}); + var self = Metamaps.Import; + var topic1 = Metamaps.Topics.get(self.cidMappings[node1_id]); + var topic2 = Metamaps.Topics.get(self.cidMappings[node2_id]); var node1 = topic1.get('node'); var node2 = topic2.get('node'); // TODO check if topic1 and topic2 were sucessfully found... @@ -275,13 +279,13 @@ Metamaps.Import = { desc: description, category: category, permission: permission, - node1_id: node1_id, - node2_id: node2_id, + node1_id: node1.id, + node2_id: node2.id, }); var mapping = new Metamaps.Backbone.Mapping({ mappable_type: "Synapse", - mappable_id: synapse.cid, + mappable_id: synapse.id, }); Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, true); diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index 2eb08b70..85b12d48 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -377,7 +377,7 @@ Metamaps.Backbone.init = function () { mappable_id: this.isNew() ? this.cid : this.id }); }, - createEdge: function () { + createEdge: function (providedMapping) { var mapping, mappingID; var synapseID = this.isNew() ? this.cid : this.id; @@ -391,7 +391,7 @@ Metamaps.Backbone.init = function () { }; if (Metamaps.Active.Map) { - mapping = this.getMapping(); + mapping = providedMapping || this.getMapping(); mappingID = mapping.isNew() ? mapping.cid : mapping.id; edge.data.$mappings = []; edge.data.$mappingIDs = [mappingID]; @@ -4614,7 +4614,7 @@ Metamaps.Synapse = { var edgeOnViz; - var newedge = synapse.createEdge(); + var newedge = synapse.createEdge(mapping); Metamaps.Visualize.mGraph.graph.addAdjacence(node1, node2, newedge.data); edgeOnViz = Metamaps.Visualize.mGraph.graph.getAdjacence(node1.id, node2.id); From 14bdc8546bab98851b938ef224d20d0fd5e04770 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 26 Mar 2016 10:12:32 +0800 Subject: [PATCH 296/305] metacodes#show routes --- app/controllers/metacodes_controller.rb | 14 +++++++++++++- config/routes.rb | 5 ++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/app/controllers/metacodes_controller.rb b/app/controllers/metacodes_controller.rb index 77f9ba54..3480c4cd 100644 --- a/app/controllers/metacodes_controller.rb +++ b/app/controllers/metacodes_controller.rb @@ -1,5 +1,5 @@ class MetacodesController < ApplicationController - before_action :require_admin, except: [:index] + before_action :require_admin, except: [:index, :show] # GET /metacodes # GET /metacodes.json @@ -18,6 +18,18 @@ class MetacodesController < ApplicationController end end + # GET /metacodes/1.json + # GET /metacodes/Action.json + # GET /metacodes/action.json + def show + @metacode = Metacode.where('DOWNCASE(name) = ?', downcase(params[:name])).first if params[:name] + @metacode = Metacode.find(params[:id]) unless @metacode + + respond_to do |format| + format.json { render json: @metacode } + end + end + # GET /metacodes/new # GET /metacodes/new.json def new diff --git a/config/routes.rb b/config/routes.rb index c68091ed..cbbc50f2 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -23,7 +23,10 @@ Metamaps::Application.routes.draw do resources :messages, only: [:show, :create, :update, :destroy] resources :mappings, except: [:index, :new, :edit] resources :metacode_sets, :except => [:show] - resources :metacodes, :except => [:show, :destroy] + + resources :metacodes, :except => [:destroy] + get 'metacodes/:name', to: 'metacodes#show' + resources :synapses, except: [:index, :new, :edit] resources :topics, except: [:index, :new, :edit] do get :autocomplete_topic, :on => :collection From d3649f1d26bf4d8e87ed2eb4399c0760e6b9dc7e Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 26 Mar 2016 11:31:55 +0800 Subject: [PATCH 297/305] DRY map exporting with policy_scoping --- app/controllers/maps_controller.rb | 17 +++- app/models/map.rb | 44 ---------- app/services/map_export_service.rb | 84 +++++++++++++++++++ .../maps/{show.xls.erb => export.xls.erb} | 2 +- config/routes.rb | 2 + 5 files changed, 102 insertions(+), 47 deletions(-) create mode 100644 app/services/map_export_service.rb rename app/views/maps/{show.xls.erb => export.xls.erb} (74%) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index fa743d93..762f2e99 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -85,11 +85,24 @@ class MapsController < ApplicationController respond_with(@allmappers, @allmappings, @allsynapses, @alltopics, @allmessages, @map) } format.json { render json: @map } - format.csv { send_data @map.to_csv } - format.xls + format.csv { redirect_to :export } + format.xls { redirect_to :export } end end + # GET maps/:id/export + def export + map = Map.find(params[:id]) + authorize map + exporter = MapExportService(current_user, map) + respond_to do |format| + format.json { render json: exporter.json } + format.csv { send_data exporter.csv } + format.xls { @spreadsheet = exporter.xls } + end + end + + # GET maps/:id/contains def contains @map = Map.find(params[:id]) diff --git a/app/models/map.rb b/app/models/map.rb index 9efc278b..d9eb6a18 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -84,50 +84,6 @@ class Map < ActiveRecord::Base json end - def to_spreadsheet - spreadsheet = [] - spreadsheet << ["Topics"] - spreadsheet << ["Id", "Name", "Metacode", "X", "Y", "Description", "Link", "User", "Permission"] - self.topicmappings.each do |mapping| - topic = mapping.mappable - next if topic.nil? - spreadsheet << [ - topic.id, - topic.name, - topic.metacode.name, - mapping.xloc, - mapping.yloc, - topic.desc, - topic.link, - topic.user.name, - topic.permission - ] - end - spreadsheet << [] - spreadsheet << ["Synapses"] - spreadsheet << ["Id", "Description", "Category", "Topic1", "Topic2", "User", "Permission"] - self.synapses.each do |synapse| - spreadsheet << [ - synapse.id, - synapse.desc, - synapse.category, - synapse.node1_id, - synapse.node2_id, - synapse.user.name, - synapse.permission - ] - end - spreadsheet - end - - def to_csv(options = {}) - CSV.generate(options) do |csv| - to_spreadsheet.each do |line| - csv << line - end - end - end - def decode_base64(imgBase64) decoded_data = Base64.decode64(imgBase64) diff --git a/app/services/map_export_service.rb b/app/services/map_export_service.rb new file mode 100644 index 00000000..30b6109a --- /dev/null +++ b/app/services/map_export_service.rb @@ -0,0 +1,84 @@ +class MapExportService < Struct.new(:user, :map) + def json + # marshal_dump turns OpenStruct into a Hash + { + topics: exportable_topics.map(:marshal_dump), + synapses: exportable_synapses.map(:marshal_dump) + } + end + + def csv(options = {}) + CSV.generate(options) do |csv| + to_spreadsheet.each do |line| + csv << line + end + end + end + + def xls + to_spreadsheet + end + + private + + def topic_headings + [:id, :name, :metacode, :x, :y, :description, :link, :user, :permission] + end + def synapse_headings + [:topic1, :topic2, :category, :description, :user, :permission] + end + + def exportable_topics + visible_topics ||= Pundit.policy_scope!(@user, @map.topics) + topic_mappings = Mapping.includes(mappable: [:metacode, :user]) + .where(mappable: visible_topics, map: @map) + topic_mappings.map do |mapping| + topic = mapping.mappable + OpenStruct.new( + id: topic.id, + name: topic.name, + metacode: topic.metacode.name, + x: mapping.xloc, + y: mapping.yloc, + description: topic.desc, + link: topic.link, + user: topic.user.name, + permission: topic.permission + ) + end + end + + def exportable_synapses + visible_synapses = Pundit.policy_scope!(@user, @map.synapses) + visible_synapses.map do |synapse| + OpenStruct.new( + topic1: synapse.node1_id, + topic2: synapse.node2_id, + category: synapse.category, + description: synapse.desc, + user: synapse.user.name, + permission: synapse.permission + ) + end + end + + def to_spreadsheet + spreadsheet = [] + spreadsheet << ["Topics"] + spreadsheet << topic_headings.map(:capitalize) + exportable_topics.each do |topics| + # convert exportable_topics into an array of arrays + topic_headings.map do { |h| topics.send(h) } + end + + spreadsheet << [] + spreadsheet << ["Synapses"] + spreadsheet << synapse_headings.map(:capitalize) + exportable_synapses.each do |synapse| + # convert exportable_synapses into an array of arrays + synapse_headings.map do { |h| synapse.send(h) } + end + + spreadsheet + end +end diff --git a/app/views/maps/show.xls.erb b/app/views/maps/export.xls.erb similarity index 74% rename from app/views/maps/show.xls.erb rename to app/views/maps/export.xls.erb index 2f11a946..7030d501 100644 --- a/app/views/maps/show.xls.erb +++ b/app/views/maps/export.xls.erb @@ -1,5 +1,5 @@ - <% @map.to_spreadsheet.each do |line| %> + <% @spreadsheet.each do |line| %> <% line.each do |field| %> diff --git a/config/routes.rb b/config/routes.rb index cbbc50f2..a9f82d9c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -36,6 +36,8 @@ Metamaps::Application.routes.draw do get 'topics/:id/relatives', to: 'topics#relatives', as: :relatives resources :maps, except: [:index, :new, :edit] + get 'maps/:id/export', to: 'maps#export' + get 'explore/active', to: 'maps#activemaps' get 'explore/featured', to: 'maps#featuredmaps' get 'explore/mine', to: 'maps#mymaps' From 92f78aa56a729809eb60e1f05be13a9f33894e47 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 26 Mar 2016 12:49:26 +0800 Subject: [PATCH 298/305] update tsv code to handle new export code at the very least. next step will be allowing json input too --- .../javascripts/src/Metamaps.Import.js.erb | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Import.js.erb b/app/assets/javascripts/src/Metamaps.Import.js.erb index 8d6b3867..92c9566d 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js.erb +++ b/app/assets/javascripts/src/Metamaps.Import.js.erb @@ -12,22 +12,23 @@ * 6 topic6 Action -425 63 admin commons * * Synapses - * Id Description Category Topic1 Topic2 User Permission - * 43 from-to 6 2 admin commons - * 44 from-to 6 1 admin commons - * 45 from-to 6 5 admin commons - * 46 from-to 2 7 admin commons - * 47 from-to 8 6 admin commons - * 48 from-to 8 1 admin commons + * Topic1 Topic2 Category Description User Permission + * 6 2 from-to admin commons + * 6 1 from-to admin commons + * 6 5 from-to admin commons + * 2 7 from-to admin commons + * 8 6 from-to admin commons + * 8 1 from-to admin commons * */ Metamaps.Import = { + // note that user is not imported topicWhitelist: [ - 'id', 'name', 'metacode', 'description', 'link', 'permission' + 'id', 'name', 'metacode', 'x', 'y', 'description', 'link', 'permission' ], synapseWhitelist: [ - 'id', 'desc', 'description', 'category', 'topic1', 'topic2', 'permission' + 'topic1', 'topic2', 'category', 'desc', 'description', 'permission' ], cidMappings: {}, //to be filled by import_id => cid mappings From 53867caae83e5fe0b11c11489ed56cf8f96ad4fb Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 26 Mar 2016 14:20:49 +0800 Subject: [PATCH 299/305] allow JSON or TSV parsing --- app/assets/javascripts/src/Metamaps.Import.js.erb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/src/Metamaps.Import.js.erb b/app/assets/javascripts/src/Metamaps.Import.js.erb index 92c9566d..bfce14bd 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js.erb +++ b/app/assets/javascripts/src/Metamaps.Import.js.erb @@ -37,7 +37,16 @@ Metamaps.Import = { $('body').bind('paste', function(e) { var text = e.originalEvent.clipboardData.getData('text/plain'); - var results = self.parseTabbedString(text); + var results; + if (text[0] === '{') { + try { + results = JSON.parse(text); + } catch (Error e) { + results = false; + } + } else { + results = self.parseTabbedString(text); + } if (results === false) return; var topics = results.topics; From ae9f4a51a24c44ecbbe8e205cf8310f1edb3b4d4 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 26 Mar 2016 15:21:55 +0800 Subject: [PATCH 300/305] fix a few embarassing errors - export is working --- app/controllers/maps_controller.rb | 6 +++--- app/policies/map_policy.rb | 4 ++++ app/services/map_export_service.rb | 18 +++++++++--------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 762f2e99..131e5959 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -85,8 +85,8 @@ class MapsController < ApplicationController respond_with(@allmappers, @allmappings, @allsynapses, @alltopics, @allmessages, @map) } format.json { render json: @map } - format.csv { redirect_to :export } - format.xls { redirect_to :export } + format.csv { redirect_to action: :export, format: :csv } + format.xls { redirect_to action: :export, format: :xls } end end @@ -94,7 +94,7 @@ class MapsController < ApplicationController def export map = Map.find(params[:id]) authorize map - exporter = MapExportService(current_user, map) + exporter = MapExportService.new(current_user, map) respond_to do |format| format.json { render json: exporter.json } format.csv { send_data exporter.csv } diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 65f721bf..5b4bbfa9 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -31,6 +31,10 @@ class MapPolicy < ApplicationPolicy record.permission == 'commons' || record.permission == 'public' || record.user == user end + def export? + show? + end + def contains? show? end diff --git a/app/services/map_export_service.rb b/app/services/map_export_service.rb index 30b6109a..94a0cf17 100644 --- a/app/services/map_export_service.rb +++ b/app/services/map_export_service.rb @@ -2,8 +2,8 @@ class MapExportService < Struct.new(:user, :map) def json # marshal_dump turns OpenStruct into a Hash { - topics: exportable_topics.map(:marshal_dump), - synapses: exportable_synapses.map(:marshal_dump) + topics: exportable_topics.map(&:marshal_dump), + synapses: exportable_synapses.map(&:marshal_dump) } end @@ -29,9 +29,9 @@ class MapExportService < Struct.new(:user, :map) end def exportable_topics - visible_topics ||= Pundit.policy_scope!(@user, @map.topics) + visible_topics ||= Pundit.policy_scope!(user, map.topics) topic_mappings = Mapping.includes(mappable: [:metacode, :user]) - .where(mappable: visible_topics, map: @map) + .where(mappable: visible_topics, map: map) topic_mappings.map do |mapping| topic = mapping.mappable OpenStruct.new( @@ -49,7 +49,7 @@ class MapExportService < Struct.new(:user, :map) end def exportable_synapses - visible_synapses = Pundit.policy_scope!(@user, @map.synapses) + visible_synapses = Pundit.policy_scope!(user, map.synapses) visible_synapses.map do |synapse| OpenStruct.new( topic1: synapse.node1_id, @@ -65,18 +65,18 @@ class MapExportService < Struct.new(:user, :map) def to_spreadsheet spreadsheet = [] spreadsheet << ["Topics"] - spreadsheet << topic_headings.map(:capitalize) + spreadsheet << topic_headings.map(&:capitalize) exportable_topics.each do |topics| # convert exportable_topics into an array of arrays - topic_headings.map do { |h| topics.send(h) } + spreadsheet << topic_headings.map { |h| topics.send(h) } end spreadsheet << [] spreadsheet << ["Synapses"] - spreadsheet << synapse_headings.map(:capitalize) + spreadsheet << synapse_headings.map(&:capitalize) exportable_synapses.each do |synapse| # convert exportable_synapses into an array of arrays - synapse_headings.map do { |h| synapse.send(h) } + spreadsheet << synapse_headings.map { |h| synapse.send(h) } end spreadsheet From f9e62496157264e8fba08191f61add2d57f13470 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 27 Mar 2016 11:46:58 +0800 Subject: [PATCH 301/305] Fix up import - want more backboney event listening though --- .../javascripts/src/Metamaps.Import.js.erb | 22 ++++++++++++++----- app/assets/javascripts/src/Metamaps.js.erb | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Import.js.erb b/app/assets/javascripts/src/Metamaps.Import.js.erb index bfce14bd..9558653f 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js.erb +++ b/app/assets/javascripts/src/Metamaps.Import.js.erb @@ -34,14 +34,17 @@ Metamaps.Import = { init: function() { var self = Metamaps.Import; + $('body').bind('paste', function(e) { + if (e.target.tagName === "INPUT") return; + var text = e.originalEvent.clipboardData.getData('text/plain'); var results; if (text[0] === '{') { try { results = JSON.parse(text); - } catch (Error e) { + } catch (e) { results = false; } } else { @@ -63,7 +66,6 @@ Metamaps.Import = { }, abort: function(message) { - alert("Sorry, something went wrong!\n\n" + message); console.error(message); }, @@ -283,20 +285,28 @@ Metamaps.Import = { var topic2 = Metamaps.Topics.get(self.cidMappings[node2_id]); var node1 = topic1.get('node'); var node2 = topic2.get('node'); - // TODO check if topic1 and topic2 were sucessfully found... + + if (topic1.isNew() || topic2.isNew()) { + return setTimeout(function() { + self.createSynapseWithParameters(description, category, permission, + node1_id, node2_id) + }, 200); + } var synapse = new Metamaps.Backbone.Synapse({ desc: description, category: category, permission: permission, - node1_id: node1.id, - node2_id: node2.id, + node1_id: topic1.id, + node2_id: topic2.id }); + Metamaps.Synapses.add(synapse); var mapping = new Metamaps.Backbone.Mapping({ mappable_type: "Synapse", - mappable_id: synapse.id, + mappable_id: synapse.cid, }); + Metamaps.Mappings.add(mapping); Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, true); }, diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/app/assets/javascripts/src/Metamaps.js.erb index 85b12d48..7a9d803b 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/app/assets/javascripts/src/Metamaps.js.erb @@ -4683,7 +4683,7 @@ Metamaps.Synapse = { node1 = synapsesToCreate[i]; topic1 = node1.getData('topic'); synapse = new Metamaps.Backbone.Synapse({ - desc: Metamaps.Create.newSynapse.description,// || "", + desc: Metamaps.Create.newSynapse.description, node1_id: topic1.isNew() ? topic1.cid : topic1.id, node2_id: topic2.isNew() ? topic2.cid : topic2.id, }); From a82b0048d86796cb6b91ac8882b7580f0078843e Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 27 Mar 2016 00:14:22 -0700 Subject: [PATCH 302/305] don't need sequenced --- Gemfile | 1 - app/models/event.rb | 3 --- db/migrate/20160312234946_create_events.rb | 2 -- 3 files changed, 6 deletions(-) diff --git a/Gemfile b/Gemfile index 3eab4256..1a129d7b 100644 --- a/Gemfile +++ b/Gemfile @@ -19,7 +19,6 @@ gem 'uservoice-ruby' gem 'dotenv' gem 'snorlax' gem 'httparty' -gem 'sequenced', '~> 2.0.0' gem 'active_model_serializers', '~> 0.8.1' gem 'delayed_job', '~> 4.0.2' gem 'delayed_job_active_record', '~> 4.0.1' diff --git a/app/models/event.rb b/app/models/event.rb index b49b4856..6dfb7e26 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -6,7 +6,6 @@ class Event < ActiveRecord::Base belongs_to :map belongs_to :user - scope :sequenced, -> { where('sequence_id is not null').order('sequence_id asc') } scope :chronologically, -> { order('created_at asc') } after_create :notify_webhooks!, if: :map @@ -14,8 +13,6 @@ class Event < ActiveRecord::Base validates_inclusion_of :kind, :in => KINDS validates_presence_of :eventable - acts_as_sequenced scope: :map_id, column: :sequence_id, skip: lambda {|e| e.map.nil? || e.map_id.nil? } - #def notify!(user) # notifications.create!(user: user) #end diff --git a/db/migrate/20160312234946_create_events.rb b/db/migrate/20160312234946_create_events.rb index 7ab71099..4ce01e35 100644 --- a/db/migrate/20160312234946_create_events.rb +++ b/db/migrate/20160312234946_create_events.rb @@ -5,9 +5,7 @@ class CreateEvents < ActiveRecord::Migration t.references :eventable, polymorphic: true, index: true t.references :user, index: true t.references :map, index: true - t.integer :sequence_id, index: true, default: nil, null: true t.timestamps end - add_index :events, [:map_id, :sequence_id], unique: true end end From 30d327f07aec12efb079d258735671d1dc1c396c Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 27 Mar 2016 15:20:09 +0800 Subject: [PATCH 303/305] solution using backbone events instead of setTimeout --- .../javascripts/src/Metamaps.Import.js.erb | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Import.js.erb b/app/assets/javascripts/src/Metamaps.Import.js.erb index 9558653f..f30f65e5 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js.erb +++ b/app/assets/javascripts/src/Metamaps.Import.js.erb @@ -70,7 +70,7 @@ Metamaps.Import = { }, simplify: function(string) { - return string + return string; .replace(/(^\s*|\s*$)/g, '') .toLowerCase(); }, @@ -239,10 +239,28 @@ Metamaps.Import = { var self = Metamaps.Import; parsedSynapses.forEach(function(synapse) { - self.createSynapseWithParameters( - synapse.desc, synapse.category, synapse.permission, - synapse.topic1, synapse.topic2 - ); + //only createSynapseWithParameters once both topics are persisted + var topic1 = Metamaps.Topics.get(self.cidMappings[node1_id]); + var topic2 = Metamaps.Topics.get(self.cidMappings[node2_id]); + var synapse_created = false + topic1.once('sync', function() { + if (topic1.id && topic2.id && !synapse_created) { + synaprse_created = true + self.createSynapseWithParameters( + synapse.desc, synapse.category, synapse.permission, + topic1, topic2 + ); + }//if + }); + topic2.once('sync', function() { + if (topic1.id && topic2.id && !synapse_created) { + synaprse_created = true + self.createSynapseWithParameters( + synapse.desc, synapse.category, synapse.permission, + topic1, topic2 + ); + }//if + }); }); }, @@ -279,19 +297,15 @@ Metamaps.Import = { }, createSynapseWithParameters: function(description, category, permission, - node1_id, node2_id) { + topic1, topic2) { var self = Metamaps.Import; - var topic1 = Metamaps.Topics.get(self.cidMappings[node1_id]); - var topic2 = Metamaps.Topics.get(self.cidMappings[node2_id]); var node1 = topic1.get('node'); var node2 = topic2.get('node'); - if (topic1.isNew() || topic2.isNew()) { - return setTimeout(function() { - self.createSynapseWithParameters(description, category, permission, - node1_id, node2_id) - }, 200); - } + if (!topic1.id || !topic2.id) { + console.error("missing topic id when creating synapse") + return; + }//if var synapse = new Metamaps.Backbone.Synapse({ desc: description, From 49f4b2030e0b7a6dd9e796f32a00f17db1cb8121 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 27 Mar 2016 00:21:32 -0700 Subject: [PATCH 304/305] gemfile.lock needed to change for travis to be happy --- Gemfile.lock | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 96aaa9fd..6a522642 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -221,9 +221,6 @@ GEM sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) tilt (>= 1.1, < 3) - sequenced (2.0.0) - activerecord (>= 3.0) - activesupport (>= 3.0) shoulda-matchers (3.1.1) activesupport (>= 4.0.0) simplecov (0.11.2) @@ -295,7 +292,6 @@ DEPENDENCIES redis rspec-rails sass-rails - sequenced (~> 2.0.0) shoulda-matchers simplecov snorlax From 8a6e702c12ff6ea172d62fa07340f1e37f07c2df Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 27 Mar 2016 15:32:18 +0800 Subject: [PATCH 305/305] fix js error --- app/assets/javascripts/src/Metamaps.Import.js.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/src/Metamaps.Import.js.erb b/app/assets/javascripts/src/Metamaps.Import.js.erb index f30f65e5..def71168 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js.erb +++ b/app/assets/javascripts/src/Metamaps.Import.js.erb @@ -70,7 +70,7 @@ Metamaps.Import = { }, simplify: function(string) { - return string; + return string .replace(/(^\s*|\s*$)/g, '') .toLowerCase(); },
    <%= field %>