diff --git a/.gitignore b/.gitignore index 050c9d9..2f53496 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,7 @@ /log/* !/log/.keep /tmp + +/data/* + +dump.rdb \ No newline at end of file diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..83e16f8 --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--require spec_helper diff --git a/.vagrant/machines/nox/virtualbox/action_provision b/.vagrant/machines/nox/virtualbox/action_provision new file mode 100644 index 0000000..6ac823e --- /dev/null +++ b/.vagrant/machines/nox/virtualbox/action_provision @@ -0,0 +1 @@ +1.5:78672f12-18ae-4cd7-8606-7073530c02a2 \ No newline at end of file diff --git a/.vagrant/machines/nox/virtualbox/action_set_name b/.vagrant/machines/nox/virtualbox/action_set_name new file mode 100644 index 0000000..cbd1fc2 --- /dev/null +++ b/.vagrant/machines/nox/virtualbox/action_set_name @@ -0,0 +1 @@ +1449329777 \ No newline at end of file diff --git a/.vagrant/machines/nox/virtualbox/creator_uid b/.vagrant/machines/nox/virtualbox/creator_uid new file mode 100644 index 0000000..ec52cb8 --- /dev/null +++ b/.vagrant/machines/nox/virtualbox/creator_uid @@ -0,0 +1 @@ +501 \ No newline at end of file diff --git a/.vagrant/machines/nox/virtualbox/id b/.vagrant/machines/nox/virtualbox/id new file mode 100644 index 0000000..3a96458 --- /dev/null +++ b/.vagrant/machines/nox/virtualbox/id @@ -0,0 +1 @@ +78672f12-18ae-4cd7-8606-7073530c02a2 \ No newline at end of file diff --git a/.vagrant/machines/nox/virtualbox/index_uuid b/.vagrant/machines/nox/virtualbox/index_uuid new file mode 100644 index 0000000..727ceb1 --- /dev/null +++ b/.vagrant/machines/nox/virtualbox/index_uuid @@ -0,0 +1 @@ +d1b79d4f7a9d4b558848ddcb5403877d \ No newline at end of file diff --git a/.vagrant/machines/nox/virtualbox/private_key b/.vagrant/machines/nox/virtualbox/private_key new file mode 100644 index 0000000..79f8c9c --- /dev/null +++ b/.vagrant/machines/nox/virtualbox/private_key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEA0biLLIvog+G2O6O/pLfYPBQQjSCWQbfowrUqfSEQb8RysEH7 +sIJs6xDFS8ovasOueiCgSdiB/6nbFjPP8VHpfVSvSSK0CcQk4XGnldPhhex35jlp +QzHubCvLW3sophMHRDehf76971Wd1QwPWgJazuvwblyKm70GeEcwQR/QBnLBy4T0 +/YjhMXpBRDWmFUCTkb7p2kQeIfLZbbinsTj8ytY9YT6PYRX0cYnJmwYV1qPePIru +jNhBoEDSkyanCXnwhteJK3uRyJTUEUIXmw429WUxrzloOTyQQAoHXAXvTvFwMZCL +bh7GmhxrHJC5HSHWZ/4d///nHWH1++2sF2xhPQIDAQABAoIBAQDEgGN248iC+ZPk +IbPJRLEI6cvwT945yXYAKfubrsVV0/2aBNktM6eWQOp77v+qy5rJt5Q4XPLBeIdd +MELgW92onxZ2Mlv64puj2PgrPJINB9n4D0b/vOMm24n5N1aI9T9TvcRGi5QvkksG +efxQW/B1/UAUSAVfkydiv2EJRCOIRX9htFx5rzHIP3hDw8AL7nl890DFL6tXiYBo +vbvmSADnisChew+E01DSxLnoxHtERtq+9tEtLQJY6+aoOaRg1GSwjqTXe9mUxRPM +0Y/ne1u1zxXt9VocOFSN9o1Palluh00U2tWuCry1mr+Qpq4/OyxI8TciGqFCr94H +ZEujgkkJAoGBAPP2CsueBY23FbiMqTxEfiRzv47v2bI9SXybBPmQMWm7l68sprmI +6xWBgxETL6/pqLPSijWf/CT6lzPMReeGnp4+Rua0OI854/Gp3TcALn64S078c1A4 +piH9H5ZaWRlNSRhSRU0Po5iSEM2mKRvgotYpJNF3afDUVY5Kg2DGk77vAoGBANwR +8e97h6W51tp/ryZnjJLFx2Z4YlTZtdg/qV0M/nCAKqbvUHu8lyP72fF219cpA4qC +ef0nmZGYcAPdmPsZuonioMVkm1zfF04sGwZwL8rYF4bo7GXji3JHcE1HUKottlYA +/wp4U7rXXMNxwm1pA8sKY3d+VMMPUfUMoYmCZiKTAoGBAMMXmlB0wSowJG2eBuRM +Pbf23FR5GFVCT5cW/OZ6Whmcy9NpWLb8eEqNdHveJP9/Usri7mWt19zWjL3+eFSL +QiN32Ak8TBK1j8S9O0t1mLj7tjWnCqw3cRuzKWR6QdBLDs4lVIgonoIvJMLgQvWp +MW8kHe5omU7e7sBIdEGa66H/AoGAEo5Yzg6mc2zmFuppRF261q1ikNtZvznUQXWs +vDHaSnYkIotPR/+w5tHXoKqarIPCzq0NyDDMnCA0Yb8PpSyYNAQt9jbzerM87dR+ +Ot6+yOXLpg6B0F2NZodririWrqLIGxxeZO2cccazBa/T6xHNxhMMLAk08HWcPYNh +I40hO40CgYEA7ggFL3LiDEZcDIss4U6vBhGQncRqfmyqAiLrqhby2vKRV+gnR+Fe +UYIRF7IxO2BBKJw3lFQR3YsRhqeSM4q13X//UAippJQlNZX0dPQsrVNfp6tU+fNF +SyGaWggqPDuY7JVHS+cMXWBW9nWBuBRY8AqaadTlCC/6irkUlr9UStY= +-----END RSA PRIVATE KEY----- diff --git a/.vagrant/machines/nox/virtualbox/synced_folders b/.vagrant/machines/nox/virtualbox/synced_folders new file mode 100644 index 0000000..5a0a1c8 --- /dev/null +++ b/.vagrant/machines/nox/virtualbox/synced_folders @@ -0,0 +1 @@ +{"virtualbox":{"/vagrant":{"guestpath":"/vagrant","hostpath":"/Users/gamerinshaft/rails_app/switch_api","disabled":false},"/tmp/vagrant-puppet/manifests-165a8fe60decd8583fc5c43cbcd1c9cf":{"owner":"root","nfs__quiet":true,"guestpath":"/tmp/vagrant-puppet/manifests-165a8fe60decd8583fc5c43cbcd1c9cf","hostpath":"/Users/gamerinshaft/rails_app/switch_api/puppetmanifests","disabled":false},"/tmp/vagrant-puppet/modules-5f5d0cebfa4e3f108c5e8c7b7edd953b":{"owner":"root","nfs__quiet":true,"guestpath":"/tmp/vagrant-puppet/modules-5f5d0cebfa4e3f108c5e8c7b7edd953b","hostpath":"/Users/gamerinshaft/rails_app/switch_api/modules","disabled":false}}} \ No newline at end of file diff --git a/Gemfile b/Gemfile index 463a2cf..56fd077 100644 --- a/Gemfile +++ b/Gemfile @@ -11,7 +11,7 @@ gem 'uglifier', '>= 1.3.0' # Use CoffeeScript for .coffee assets and views gem 'coffee-rails', '~> 4.1.0' # See https://github.com/rails/execjs#readme for more supported runtimes -# gem 'therubyracer', platforms: :ruby +gem 'therubyracer', platforms: :ruby # Use jquery as the JavaScript library gem 'jquery-rails' @@ -37,6 +37,12 @@ gem 'grape-jbuilder' gem 'grape-swagger' gem 'grape-swagger-ui' gem 'haml-rails' +gem 'redis' +gem 'resque' +gem 'resque-scheduler' +gem 'rubocop', group: :development +gem 'kakurenbo-puti' +gem 'gon' group :development, :test do gem 'byebug' @@ -44,7 +50,6 @@ group :development, :test do gem 'web-console', '~> 2.0' gem 'spring' - gem 'rubocop' gem 'rails-erd' gem 'pry-rails' gem 'pry-doc' @@ -55,5 +60,9 @@ group :development, :test do gem 'awesome_print' gem 'quiet_assets' gem 'annotate' - gem 'rspec-rails', '~> 3.0.0.beta2' + gem 'json_expressions' + gem 'rspec-rails', '~> 3.0.0' + gem 'factory_girl_rails' + gem 'faker' + gem 'database_cleaner' end diff --git a/Gemfile.lock b/Gemfile.lock index e114a1a..16b0591 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -52,7 +52,7 @@ GEM binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) builder (3.2.2) - byebug (8.2.0) + byebug (8.2.1) choice (0.2.0) coderay (1.1.0) coercible (1.0.0) @@ -64,6 +64,8 @@ GEM coffee-script-source execjs coffee-script-source (1.10.0) + concurrent-ruby (1.0.0) + database_cleaner (1.5.1) debug_inspector (0.0.2) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) @@ -71,8 +73,20 @@ GEM equalizer (0.0.11) erubis (2.7.0) execjs (2.6.0) + factory_girl (4.5.0) + activesupport (>= 3.0.0) + factory_girl_rails (4.5.0) + factory_girl (~> 4.5.0) + railties (>= 3.0.0) + faker (1.6.1) + i18n (~> 0.5) globalid (0.3.6) activesupport (>= 4.1.0) + gon (6.0.1) + actionpack (>= 3.0) + json + multi_json + request_store (>= 1.0) grape (0.13.0) activesupport builder @@ -125,18 +139,23 @@ GEM railties (>= 4.2.0) thor (>= 0.14, < 2.0) json (1.8.3) + json_expressions (0.8.3) + kakurenbo-puti (0.1.0) + activerecord (>= 4.1.0) + libv8 (3.16.14.13) loofah (2.0.3) nokogiri (>= 1.5.9) mail (2.6.3) mime-types (>= 1.16, < 3) method_source (0.8.2) - mime-types (2.6.2) - mini_portile (0.6.2) + mime-types (2.99) + mini_portile2 (2.0.0) minitest (5.8.3) + mono_logger (1.1.0) multi_json (1.11.2) multi_xml (0.5.5) - nokogiri (1.6.6.4) - mini_portile (~> 0.6.0) + nokogiri (1.6.7) + mini_portile2 (~> 2.0.0.rc2) parser (2.2.3.0) ast (>= 1.1, < 3.0) powerpack (0.1.1) @@ -159,6 +178,8 @@ GEM rack (>= 0.4) rack-mount (0.8.3) rack (>= 1.0.0) + rack-protection (1.5.3) + rack rack-test (0.6.3) rack (>= 1.0) rails (4.2.1) @@ -193,13 +214,28 @@ GEM rainbow (2.0.0) rake (10.4.2) rdoc (4.2.0) - json (~> 1.4) - rspec-core (3.0.3) + redis (3.2.2) + redis-namespace (1.5.2) + redis (~> 3.0, >= 3.0.4) + ref (2.0.0) + request_store (1.2.1) + resque (1.25.2) + mono_logger (~> 1.0) + multi_json (~> 1.0) + redis-namespace (~> 1.3) + sinatra (>= 0.9.2) + vegas (~> 0.1.2) + resque-scheduler (4.0.0) + mono_logger (~> 1.0) + redis (~> 3.0) + resque (~> 1.25) + rufus-scheduler (~> 3.0) + rspec-core (3.0.4) rspec-support (~> 3.0.0) - rspec-expectations (3.0.3) + rspec-expectations (3.0.4) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.0.0) - rspec-mocks (3.0.3) + rspec-mocks (3.0.4) rspec-support (~> 3.0.0) rspec-rails (3.0.2) actionpack (>= 3.0) @@ -209,7 +245,7 @@ GEM rspec-expectations (~> 3.0.0) rspec-mocks (~> 3.0.0) rspec-support (~> 3.0.0) - rspec-support (3.0.3) + rspec-support (3.0.4) rubocop (0.35.1) astrolabe (~> 1.3) parser (>= 2.2.3.0, < 3.0) @@ -219,8 +255,9 @@ GEM tins (<= 1.6.0) ruby-graphviz (1.2.2) ruby-progressbar (1.7.5) - ruby_parser (3.7.1) + ruby_parser (3.7.2) sexp_processor (~> 4.1) + rufus-scheduler (3.1.10) sass (3.4.19) sass-rails (5.0.4) railties (>= 4.0.0, < 5.0) @@ -232,9 +269,14 @@ GEM json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) sexp_processor (4.6.0) + sinatra (1.4.6) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (>= 1.3, < 3) slop (3.6.0) - spring (1.4.4) - sprockets (3.4.0) + spring (1.5.0) + sprockets (3.5.0) + concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (2.3.3) actionpack (>= 3.0) @@ -243,6 +285,9 @@ GEM sqlite3 (1.3.11) tapp (1.5.0) thor + therubyracer (0.12.2) + libv8 (~> 3.16.14.0) + ref thor (0.19.1) thread_safe (0.3.5) tilt (2.0.1) @@ -258,6 +303,8 @@ GEM execjs (>= 0.3.0) json (>= 1.8.0) unicode-display_width (0.1.1) + vegas (0.1.11) + rack (>= 1.0.0) virtus (1.0.5) axiom-types (~> 0.1) coercible (~> 1.0) @@ -279,6 +326,10 @@ DEPENDENCIES bcrypt (~> 3.1.7) byebug coffee-rails (~> 4.1.0) + database_cleaner + factory_girl_rails + faker + gon grape grape-jbuilder grape-swagger @@ -288,19 +339,25 @@ DEPENDENCIES hirb-unicode jbuilder (~> 2.0) jquery-rails + json_expressions + kakurenbo-puti pry-byebug pry-doc pry-rails quiet_assets rails (= 4.2.1) rails-erd - rspec-rails (~> 3.0.0.beta2) + redis + resque + resque-scheduler + rspec-rails (~> 3.0.0) rubocop sass-rails (~> 5.0) sdoc (~> 0.4.0) spring sqlite3 tapp + therubyracer turbolinks uglifier (>= 1.3.0) web-console (~> 2.0) diff --git a/README.md b/README.md new file mode 100644 index 0000000..a9fc65b --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +# Switch API + +大学のプロジェクト課題で作成したAPIサーバーです。 +家電製品を動かしたいという欲望で動いてるデザイアドリブン駆動なプロジェクトです。 + +基本git flowの思想の元ブランチは切っていて、 +circleCIでテストを動かして通らない場合はプルリクを受け付けない方針をとっています。 + +## 下準備 + +クローンしたプロジェクト内のプログラムをコンパイルする + +### 受信コマンドコンパイル + +```sh +$ sudo gcc receive.c -o receive -lwiringPi +``` + +### 送信コマンドコンパイル + +```sh +$ sudo gcc send.c -lm -o send -lwiringPi +``` + +## サーバーを立てる + +一部権限を求められるshコマンドが含まれているので、`sudo`をつけると間違いないかも + +### Rails立ち上げ + +```sh +$ sudo rails s +``` +### Redis立ち上げ + +```sh +$ redis-server +``` + +### Resque立ち上げ + +```sh +$ sudo QUEUE=* rake environment resque:work +``` + +### scheduler立ち上げ + +```sh +$ DYNAMIC_SCHEDULE=true rake environment resque:scheduler +``` + +### ログを監視 + +```sh +$ tail -f log/outputFileName +``` + + +## スケジューラーに関して + +cron形式でデータを渡す時の値の諸々 + +### cronの設定 + +左から、[分] [時] [日] [月] [曜日] [コマンド] + +- 分は0~59の数字で指定 +- 時は0~23の数字で指定 +- 日は1~31の数字で指定 +- 月は1~12の数字で指定 +- 曜日に関しても数字で指定し、0と7が日曜日、1以降は順に、月、火、水、木、金、土となる + +コマンドは、設定ファイルでパスを通していないものに関してはフルパスで指定するかカレントディレクトリからの相対パスで指定しなければならない、指定はこちらでするのでクライアントから引数を渡す時は曜日までの引数で良い + +``` +43 23 * * * 23:43に実行 +12 05 * * * 05:12に実行 +0 17 * * * 17:00に実行 +0 17 * * 1 毎週月曜の 17:00に実行 +0,10 17 * * 0,2,3 毎週日,火,水曜の 17:00と 17:10に実行 +0-10 17 1 * * 毎月 1日の 17:00から17:10まで 1分毎に実行 +0 0 1,15 * 1 毎月 1日と 15日と 月曜日の 0:00に実行 +42 4 1 * * 毎月 1日の 4:42分に実行 +0 21 * * 1-6 月曜日から土曜まで 21:00に実行 +0,10,20,30,40,50 * * * * 10分おきに実行 +*/10 * * * * 10分おきに実行 +* 1 * * * 1:00から 1:59まで 1分おきに実行 +0 1 * * * 1:00に実行 +0 */1 * * * 毎時 0分に 1時間おきに実行 +0 * * * * 毎時 0分に 1時間おきに実行 +2 8-20/3 * * * 8:02,11:02,14:02,17:02,20:02に実行 +30 5 1,15 * * 1日と 15日の 5:30に実行 +``` + +## デバッグに関して + +### APIのテスト用ページ + +```sh + SITE_URL/api/swagger +``` + +### Resque Schedulerのログページ + +```sh + SITE_URL/resque +``` + +## その他 + +データの同期に関して + +### 本番サーバーからdumpファイルを作成 + +```sh +$ scp pi@hostname:~/rails_app/switch_api/db/development.sqlite3 ./dump.sqlite3 +``` + +### 赤外線情報ファイルの取得 + +```sh +$ scp -r pi@hostname:~/rails_app/switch_api/data ./ +``` + +## Vagrantで実行する場合 + +```sh +$ vagrant up + +$ vagrant ssh + +$ cd /vagrant/ + +$ rails s -b 0.0.0.0 +``` diff --git a/README.rdoc b/README.rdoc deleted file mode 100644 index dd4e97e..0000000 --- a/README.rdoc +++ /dev/null @@ -1,28 +0,0 @@ -== README - -This README would normally document whatever steps are necessary to get the -application up and running. - -Things you may want to cover: - -* Ruby version - -* System dependencies - -* Configuration - -* Database creation - -* Database initialization - -* How to run the test suite - -* Services (job queues, cache servers, search engines, etc.) - -* Deployment instructions - -* ... - - -Please feel free to use a different markup language if you do not plan to run -rake doc:app. diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..8e908a1 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,29 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.configure('2') do |config| + # All Vagrant configuration is done here. The most common configuration + # options are documented and commented below. For a complete reference, + # please see the online documentation at vagrantup.com. + + config.vm.define :nox do |box| + # Every Vagrant virtual environment requires a box to build off of. + box.vm.box = 'precise64' + # The url from where the 'config.vm.box' box will be fetched if it + # doesn't already exist on the user's system. + box.vm.box_url = 'http://files.vagrantup.com/precise64.box' + + # Boot with a GUI so you can see the screen. (Default is headless) + # box.vm.boot_mode = :gui + # add a hostonly network if desired + # box.vm.network :hostonly, "33.33.33.151" + box.vm.network :forwarded_port, guest: 3000, host: 4000 + + box.vm.provision :puppet do |puppet| + puppet.manifests_path = 'puppetmanifests' + puppet.manifest_file = 'raspberry-nox.pp' + puppet.module_path = 'modules' + puppet.options = ['--verbose', '--debug'] + end + end +end diff --git a/app/apis/api/base.rb b/app/apis/api/base.rb index cf29158..8768cf5 100644 --- a/app/apis/api/base.rb +++ b/app/apis/api/base.rb @@ -14,11 +14,10 @@ def save_object(object) code: ErrorCodes::FAIL_SAVE } end - error!(json: { + error!(meta: { + status: 400, errors: errors - }, status: 400 - ) - false + }, response: {}) end end @@ -26,24 +25,41 @@ def check_password(user_info, raw_password) if BCrypt::Password.new(user_info.hashed_password) == raw_password true else - error!(json:{ - errors:[ - { - message: 'errors.messages.invalid_pin', - code: ErrorCodes::INVALID_PIN - } - ] - }, status: 400 - ) + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_pin'), + code: ErrorCodes::INVALID_PIN + ] + }, response: {}) false end end def user return @user if @user - return nil unless (token = AuthToken.find_by(token: params[:auth_token])) - update_auth_token token - @user = token.user + if (token = AuthToken.find_by(token: params[:auth_token])) + update_auth_token token + unless token.user.destroyed? + @user = token.user + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_auth_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_auth_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end end def update_auth_token(token) @@ -57,15 +73,13 @@ def user_signed_in? def authenticate_user! unless user_signed_in? - error!(json: { + error!(meta: { + status: 400, errors: [ - { - message: t('errors.messages.invalid_auth_token'), - code: ErrorCodes::INVALID_TOKEN - } + message: ('errors.messages.invalid_auth_token'), + code: ErrorCodes::INVALID_TOKEN ] - }, status: 400 - ) + }, response: {}) end end end diff --git a/app/apis/api/v1/authorize.rb b/app/apis/api/v1/authorize.rb index 9d575db..25b2e48 100644 --- a/app/apis/api/v1/authorize.rb +++ b/app/apis/api/v1/authorize.rb @@ -2,42 +2,130 @@ module API module V1 class Authorize < Grape::API helpers do - params :attributes do + params :signup_params do requires :screen_name, type: String, desc: '表示名' requires :email, type: String, desc: 'メールアドレス' requires :password, type: String, desc: 'パスワード' requires :auth_token, type: String, desc: 'トークン' # optional :body, type: String, desc: "MessageBoard body." end + params :login_params do + requires :email_or_screen_name, type: String, desc: 'メールアドレスまたは名前' + requires :password, type: String, desc: 'パスワード' + end + def find_user_by_identifier(identifier) + if (user_info = UserInfo.find_by(screen_name: identifier) || UserInfo.find_by(email: identifier)) + user_info.user + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + end + end end resource :auth do - desc '確認用のテストAPI', notes: <<-NOTE + desc 'トークンの取得', notes: <<-NOTE +
+ このURLにリクエストすることによって、アクセストークンを取得することができます。
+ アクセストークンは基本的にどんなリクエストをする時でも必要なので、値をキャッシュするようにしてください。
+
User登録をします。
+ emailまたはスクリーンネームを用いてログインします。
+
+ トークンを削除してログアウトします。ユーザー情報がまだ作成されてないトークンは削除できません。 +
+ NOTE + params do + requires :auth_token, type: String, desc: 'トークン' + end + delete '/logout', jbuilder: 'api/v1/auth/logout' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if token.user.info + token.destroy + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.info_not_found'), + code: ErrorCodes::NOT_FOUND_INFO + ] + }, response: {}) end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) end end end diff --git a/app/apis/api/v1/base.rb b/app/apis/api/v1/base.rb index dbc9151..396e512 100644 --- a/app/apis/api/v1/base.rb +++ b/app/apis/api/v1/base.rb @@ -32,6 +32,11 @@ class Base < Grape::API mount V1::Users mount V1::Authorize + mount V1::IR + mount V1::InfraredGroup + mount V1::Schedules + mount V1::Logs + mount V1::Extra add_swagger_documentation format: :json, api_version: 'v1', hide_documentation_path: true end end diff --git a/app/apis/api/v1/extra.rb b/app/apis/api/v1/extra.rb new file mode 100644 index 0000000..d1b7986 --- /dev/null +++ b/app/apis/api/v1/extra.rb @@ -0,0 +1,111 @@ +module API + module V1 + class Extra < Grape::API + resource :extra do + desc '気温を取得を始める', notes: <<-NOTE +
+ null ・・・ すべての気温の取得
+ num ・・・ 最新num件の気温を取得
+
+ グループを一覧表示します。 +
+ NOTE + params do + requires :auth_token, type: String, desc: 'Auth token.' + end + get '/', jbuilder: 'api/v1/group/index' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if user.info.nil? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + else + @groups = user.infrared_groups + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + end + + desc 'グループの作成', notes: <<-NOTE ++ グループを作成します。 +
+ NOTE + params do + requires :auth_token, type: String, desc: 'Auth token.' + requires :name, type: String, desc: 'Group name.' + end + post '/', jbuilder: 'api/v1/group/create' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if user.info.nil? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + else + @group = user.infrared_groups.create(name: params[:name]) + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + end + + desc 'グループのアップデート', notes: <<-NOTE ++ グループをアップデートします。 +
+ NOTE + params do + requires :auth_token, type: String, desc: 'Auth token.' + requires :name, type: String, desc: 'Name.' + requires :group_id, type: Integer, desc: 'Group_id.' + end + put '/', jbuilder: 'api/v1/group/update' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if user.info.nil? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + else + if group = user.infrared_groups.find_by(id: params[:group_id]) + group.update(name: params[:name]) + @group = group + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.group_not_found'), + code: ErrorCodes::NOT_FOUND + ] + }, response: {}) + end + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + end + + desc 'グループの削除', notes: <<-NOTE ++ グループを削除します。 +
+ NOTE + params do + requires :auth_token, type: String, desc: 'Auth token.' + requires :group_id, type: Integer, desc: 'Group_id.' + end + delete '/', jbuilder: 'api/v1/group/destroy' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if user.info.nil? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + else + if group = user.infrared_groups.find_by(id: params[:group_id]) + @group = group + group.destroy + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.group_not_found'), + code: ErrorCodes::NOT_FOUND + ] + }, response: {}) + end + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + end + + resource :ir do + desc '赤外線の追加', notes: <<-NOTE ++ グループに赤外線を追加します。 + カンマ区切りでir_idを渡すと複数の赤外線を一気に登録できます。 +
+ NOTE + params do + requires :auth_token, type: String, desc: 'Auth token.' + requires :group_id, type: Integer, desc: 'Group_id.' + requires :ir_id, type: String, desc: 'IR_id.' + end + post '/', jbuilder: 'api/v1/group/ir/add' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if user.info.nil? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + else + if group = user.infrared_groups.find_by(id: params[:group_id]) + if params[:ir_id] =~ /^[0-9]+$/ + if infrared = user.infrareds.without_soft_destroyed.find_by(id: params[:ir_id]) + if !group.infrareds.without_soft_destroyed.find_by(id: params[:ir_id]) + group.infrareds << infrared + log = user.logs.create(name: "「#{infrared.name}」を「#{group.name}」に追加しました", status: :add_ir) + infrared.logs << log + @group = group + @infrared = infrared + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.ir_already_existing'), + code: ErrorCodes::ALREADY_EXISTING + ] + }, response: {}) + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.ir_not_found'), + code: ErrorCodes::NOT_FOUND + ] + }, response: {}) + end + elsif params[:ir_id] =~ /^(([0-9]+)((\,||\-||\/)[0-9]+)*)$/ + infrareds = params[:ir_id].split(',') + infrareds.each do |i| + if infrared = user.infrareds.without_soft_destroyed.find_by(id: i.to_i) + unless group.infrareds.without_soft_destroyed.find_by(id: i.to_i) + group.infrareds << infrared + log = user.logs.create(name: "「#{infrared.name}」を「#{group.name}」に追加しました", status: :add_ir) + infrared.logs << log + @group = group + @infrared = infrared + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.ir_not_found'), + code: ErrorCodes::NOT_FOUND + ] + }, response: {}) + end + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_params'), + code: ErrorCodes::INVALID_PARAMS + ] + }, response: {}) + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.group_not_found'), + code: ErrorCodes::NOT_FOUND + ] + }, response: {}) + end + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + end + + desc '赤外線の削除', notes: <<-NOTE ++ グループに赤外線情報を削除します。 +
+ NOTE + params do + requires :auth_token, type: String, desc: 'Auth token.' + requires :group_id, type: Integer, desc: 'Group_id.' + requires :ir_id, type: Integer, desc: 'IR_id.' + end + delete '/', jbuilder: 'api/v1/group/ir/remove' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if user.info.nil? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + else + if group = user.infrared_groups.find_by(id: params[:group_id]) + if infrared = user.infrareds.without_soft_destroyed.find_by(id: params[:ir_id]) + if relational = group.infrared_relationals.find_by(infrared_id: params[:ir_id]) + @group = group + @infrared = infrared + relational.destroy + log = user.logs.create(name: "「#{infrared.name}」を「#{group.name}」から削除しました", status: :remove_ir) + infrared.logs << log + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.ir_not_found_in_group'), + code: ErrorCodes::NOT_FOUND + ] + }, response: {}) + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.ir_not_found'), + code: ErrorCodes::NOT_FOUND + ] + }, response: {}) + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.group_not_found'), + code: ErrorCodes::NOT_FOUND + ] + }, response: {}) + end + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + end + + desc 'グループの赤外線一覧表示', notes: <<-NOTE ++ グループの赤外線を一覧表示します。 +
+ NOTE + params do + requires :auth_token, type: String, desc: 'Auth token.' + requires :group_id, type: Integer, desc: 'Group_id.' + end + get '/', jbuilder: 'api/v1/group/ir/index' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if user.info.nil? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + else + if group = user.infrared_groups.find_by(id: params[:group_id]) + @group = group + @infrareds = group.infrareds.without_soft_destroyed + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.group_not_found'), + code: ErrorCodes::NOT_FOUND + ] + }, response: {}) + end + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + end + end + end + end + end +end diff --git a/app/apis/api/v1/ir.rb b/app/apis/api/v1/ir.rb new file mode 100644 index 0000000..15fecf1 --- /dev/null +++ b/app/apis/api/v1/ir.rb @@ -0,0 +1,256 @@ +# app/apis/api/v1/ir.rb + +module API + module V1 + class IR < Grape::API + resource :ir do + desc '赤外線一覧の表示', notes: <<-NOTE ++ 赤外線一覧を表示します +
+ NOTE + params do + requires :auth_token, type: String, desc: 'Auth token.' + end + get '/', jbuilder: 'api/v1/ir/index' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if user.info.nil? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + else + @infrareds = user.infrareds.without_soft_destroyed.all + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + end + + desc '赤外線の受信', notes: <<-NOTE ++ 赤外線を受信します。 + 任意でグループIDを渡すと、そのグループに紐付けてくれます。 +
+ NOTE + params do + requires :auth_token, type: String, desc: 'Auth token.' + optional :group_id, type: Integer, desc: 'Group ID.' + end + post '/receive', jbuilder: 'api/v1/ir/receive' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if user.info.nil? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + else + infrared = user.infrareds.create(name: '名無しの赤外線', data: '') + path = Rails.root.to_s + command = File.join(path, 'commands/receive') + fname = "user_#{user.id}_ir_#{infrared.id}.txt" + `#{command} #{path}/data/#{fname}` + if File.read("#{path}/data/#{fname}").size == 0 + infrared.destroy + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.fail_scanir'), + code: ErrorCodes::FAIL_SCANIR + ] + }, response: {}) + end + infrared.update(data: "#{fname}") + log = user.logs.create(name: '赤外線を受信しました', status: :receive_ir) + infrared.logs << log + unless params[:group_id].nil? + if group = user.infrared_groups.find_by(id: params[:group_id]) + group.infrareds << infrared + log = user.logs.create(name: "「#{infrared.name}」を「#{group.name}」に追加しました", status: :add_ir) + infrared.logs << log + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.ir_accept_but_group_not_found'), + code: ErrorCodes::NOT_FOUND + ] + }, response: {}) + end + end + @infrared = infrared + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + end + desc '赤外線の送信', notes: <<-NOTE ++ 赤外線を照射します +
+ NOTE + params do + requires :auth_token, type: String, desc: 'Auth token.' + requires :ir_id, type: Integer, desc: 'IR Id.' + end + post '/send', jbuilder: 'api/v1/ir/send' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if user.info.nil? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + else + if infrared = user.infrareds.without_soft_destroyed.find_by(id: params[:ir_id]) + fname = infrared.data + path = Rails.root.to_s + command = File.join(path, 'commands/send') + `#{command} #{path}/data/#{fname}` + count = infrared.count + 1 + infrared.update(count: count) + log = user.logs.create(name: "「#{infrared.name}」を実行しました", status: :send_ir) + infrared.logs << log + @infrared = infrared + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.ir_not_found'), + code: ErrorCodes::NOT_FOUND + ] + }, response: {}) + end + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + end + desc '赤外線の名前変更', notes: <<-NOTE ++ 赤外線名を変更します +
+ NOTE + params do + requires :auth_token, type: String, desc: 'Auth token.' + requires :name, type: String, desc: 'IR name.' + requires :ir_id, type: Integer, desc: 'IR Id.' + end + put '/rename', jbuilder: 'api/v1/ir/rename' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if user.info.nil? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + else + if infrared = user.infrareds.without_soft_destroyed.find_by(id: params[:ir_id]) + infrared.update(name: params[:name]) + log = user.logs.create(name: "「#{infrared.name}」に名前を変更しました", status: :update_ir) + infrared.logs << log + @infrared = infrared + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.ir_not_found'), + code: ErrorCodes::NOT_FOUND + ] + }, response: {}) + end + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + end + desc '赤外線の削除', notes: <<-NOTE ++ 赤外線をデータベースから削除します +
+ NOTE + params do + requires :auth_token, type: String, desc: 'Auth token.' + requires :ir_id, type: Integer, desc: 'IR Id.' + end + delete '/', jbuilder: 'api/v1/ir/destroy' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if user.info.nil? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + else + if infrared = user.infrareds.without_soft_destroyed.find_by(id: params[:ir_id]) + name = infrared.data + file = Rails.root.to_s + '/data/' + name + File.delete file + log = user.logs.create(name: "「#{infrared.name}」を削除しました", status: :destroy_ir) + infrared.logs << log + infrared.soft_destroy + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.ir_not_found'), + code: ErrorCodes::NOT_FOUND + ] + }, response: {}) + end + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + end + end + end + end +end diff --git a/app/apis/api/v1/logs.rb b/app/apis/api/v1/logs.rb new file mode 100644 index 0000000..42858d7 --- /dev/null +++ b/app/apis/api/v1/logs.rb @@ -0,0 +1,81 @@ +# app/apis/api/v1/users.rb + +module API + module V1 + class Logs < Grape::API + resource :log do + desc 'logを取得する', notes: <<-NOTE +
+ null ・・・ すべてのログの取得
+ num ・・・ 最新num件のログを取得
+
+ null ・・・ すべてのログの取得
+ 0 ・・・ 赤外線にまつわるログの取得
+ 1 ・・・ スケジューラーにまつわるログの取得
+
+ null ・・・ すべてのスケジュールの取得
+ 0 ・・・ 稼働してないスケジュールの取得
+ 1 ・・・ 稼働しているスケジュールの取得
+
+ 選択したスケジューラーを稼働させるためのAPIです。 +
+ NOTE + params do + requires :auth_token, type: String, desc: 'Auth token.' + requires :schedule_id, type: Integer, desc: 'Schedule id' + end + post '/activate', jbuilder: 'api/v1/schedule/activate' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if user.info.nil? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + else + if schedule = user.schedules.without_soft_destroyed.find_by(id: params[:schedule_id]) + if schedule.active_schedule? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.schedule_already_set'), + code: ErrorCodes::ALREADY_EXISTING + ] + }, response: {}) + else + Resque.set_schedule("#{schedule.job_name}", class: 'ResqueInfraredSendJob', cron: schedule.cron, args: schedule) + schedule.update(status: :active_schedule) + log = user.logs.create(name: "「#{schedule.name}」のスケジューラーを稼働しました", status: :activate_schedule) + schedule.logs << log + @schedule = schedule + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.schedule_not_found'), + code: ErrorCodes::NOT_FOUND_SCHEDULE + ] + }, response: {}) + end + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + end + + desc 'スケジュールの停止', notes: <<-NOTE ++ cronの設定には気をつけてね +
+ NOTE + params do + requires :auth_token, type: String, desc: 'Auth token.' + requires :ir_id, type: Integer, desc: 'infrared id.' + requires :name, type: String, desc: 'name.' + requires :cron, type: String, desc: 'cron.' + end + post '/', jbuilder: 'api/v1/schedule/create' do + if (token = AuthToken.find_by(token: params[:auth_token])) + if user.info.nil? + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.user_not_found'), + code: ErrorCodes::NOT_FOUND_USER + ] + }, response: {}) + else + if (infrared = Infrared.without_soft_destroyed.find_by(id: params[:ir_id])) + cron = params[:cron] + cron_a = cron.split(' ') + translation = cron_translator(cron_a) + schedule = user.schedules.create(name: params[:name], cron: params[:cron]) + cron = params[:cron] + schedule.update(description: "#{translation}", cron: "#{cron}", job_name: "schedule_#{user.id}_#{schedule.id}") + infrared.schedule = schedule + Resque.set_schedule("#{schedule.job_name}", { class: 'ResqueInfraredSendJob', cron: cron, args: schedule }) + schedule.update(status: :active_schedule) + log = user.logs.create(name: "「#{schedule.name}」のスケジューラーを作成、稼働しました", status: :create_schedule) + schedule.logs << log + @schedule = schedule + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.ir_not_found'), + code: ErrorCodes::NOT_FOUND + ] + }, response: {}) + end + end + else + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) + end + end + + desc 'スケジュールの削除', notes: <<-NOTE +
- このURLにアクセスするとHelloを返してくれます。
- 実際にリクエストできてるか確認するためのAPIです。
-
- このURLにアクセスするとUserを作るよ。 -
- NOTE - post '/', jbuilder: 'api/v1/users/create' do - user = save_object(User.new) - @token = user.auth_tokens.new_token - end - + resource :user do desc 'ユーザー削除', notes: <<-NOTE@@ -63,111 +32,64 @@ def set_message_board use :token optional :password, type: String, desc: 'サインアップしている場合' end - delete '/', jbuilder: 'api/v1/users/destroy' do + delete '/', jbuilder: 'api/v1/user/destroy' do unless token = AuthToken.find_by(token: params[:auth_token]) - error!( json:{ - errors: [ - { - message: 'errors.messages.cant_find_token', - code: ErrorCodes::INVALID_TOKEN - } - ] - }, status: 400 - ) + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.invalid_token'), + code: ErrorCodes::INVALID_TOKEN + ] + }, response: {}) false end - if info = token.user.info if params[:password] if check_password(info, params[:password]) - user.destroy - @message = '退会しました。' + name = user.info.screen_name + user.soft_destroy + @message = "#{name}さんは退会しました。" end else - error!( json:{ - errors: [ - { - message: 'errors.messages.need_a_password', + error!(meta: { + status: 400, + errors: [ + message: ('errors.messages.need_a_password'), code: ErrorCodes::NEED_A_PASSWORD - } - ] - }, status: 400 - ) + ] + }, response: {}) false end else - user.destroy + user.soft_destroy @message = '退会しました。' end end - resource :info do desc 'ユーザー情報の表示', notes: <<-NOTE -
- このURLにアクセスするとHelloを返してくれます。
- 実際にリクエストできてるか確認するためのAPIです。
+ ユーザーのプロフィール情報がかえってきます。
=i.length)return n;var r=[],u=a[e++];return n.forEach(function(n,u){r.push({key:n,values:t(u,e)})}),u?r.sort(function(n,t){return u(n.key,t.key)}):r}var e,r,u={},i=[],a=[];return u.map=function(t,e){return n(e,t,0)},u.entries=function(e){return t(n(oa.map,e,0),0)},u.key=function(n){return i.push(n),u},u.sortKeys=function(n){return a[i.length-1]=n,u},u.sortValues=function(n){return e=n,u},u.rollup=function(n){return r=n,u},u},oa.set=function(n){var t=new m;if(n)for(var e=0,r=n.length;r>e;++e)t.add(n[e]);return t},l(m,{has:h,add:function(n){return this._[s(n+="")]=!0,n},remove:g,values:p,size:v,empty:d,forEach:function(n){for(var t in this._)n.call(this,f(t))}}),oa.behavior={},oa.rebind=function(n,t){for(var e,r=1,u=arguments.length;++r=0&&(r=n.slice(e+1),n=n.slice(0,e)),n)return arguments.length<2?this[n].on(r):this[n].on(r,t);if(2===arguments.length){if(null==t)for(n in this)this.hasOwnProperty(n)&&this[n].on(r,null);return this}},oa.event=null,oa.requote=function(n){return n.replace(wa,"\\$&")};var wa=/[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g,Sa={}.__proto__?function(n,t){n.__proto__=t}:function(n,t){for(var e in t)n[e]=t[e]},ka=function(n,t){return t.querySelector(n)},Na=function(n,t){return t.querySelectorAll(n)},Ea=function(n,t){var e=n.matches||n[x(n,"matchesSelector")];return(Ea=function(n,t){return e.call(n,t)})(n,t)};"function"==typeof Sizzle&&(ka=function(n,t){return Sizzle(n,t)[0]||null},Na=Sizzle,Ea=Sizzle.matchesSelector),oa.selection=function(){return oa.select(sa.documentElement)};var Aa=oa.selection.prototype=[];Aa.select=function(n){var t,e,r,u,i=[];n=A(n);for(var a=-1,o=this.length;++a =0?n.slice(0,t):n,r=t>=0?n.slice(t+1):"in";return e=pl.get(e)||gl,r=vl.get(r)||y,br(r(e.apply(null,la.call(arguments,1))))},oa.interpolateHcl=Rr,oa.interpolateHsl=Dr,oa.interpolateLab=Pr,oa.interpolateRound=jr,oa.transform=function(n){var t=sa.createElementNS(oa.ns.prefix.svg,"g");return(oa.transform=function(n){if(null!=n){t.setAttribute("transform",n);var e=t.transform.baseVal.consolidate()}return new Ur(e?e.matrix:dl)})(n)},Ur.prototype.toString=function(){return"translate("+this.translate+")rotate("+this.rotate+")skewX("+this.skew+")scale("+this.scale+")"};var dl={a:1,b:0,c:0,d:1,e:0,f:0};oa.interpolateTransform=$r,oa.layout={},oa.layout.bundle=function(){return function(n){for(var t=[],e=-1,r=n.length;++et;++t)(r=M[t]).index=t,r.weight=0;for(t=0;c>t;++t)r=x[t],"number"==typeof r.source&&(r.source=M[r.source]),"number"==typeof r.target&&(r.target=M[r.target]),++r.source.weight,++r.target.weight;for(t=0;u>t;++t)r=M[t],isNaN(r.x)&&(r.x=n("x",f)),isNaN(r.y)&&(r.y=n("y",v)),isNaN(r.px)&&(r.px=r.x),isNaN(r.py)&&(r.py=r.y);if(i=[],"function"==typeof h)for(t=0;c>t;++t)i[t]=+h.call(this,x[t],t);else for(t=0;c>t;++t)i[t]=h;if(a=[],"function"==typeof g)for(t=0;c>t;++t)a[t]=+g.call(this,x[t],t);else for(t=0;c>t;++t)a[t]=g;if(o=[],"function"==typeof p)for(t=0;u>t;++t)o[t]=+p.call(this,M[t],t);else for(t=0;u>t;++t)o[t]=p;return l.resume()},l.resume=function(){return l.alpha(.1)},l.stop=function(){return l.alpha(0)},l.drag=function(){return r||(r=oa.behavior.drag().origin(y).on("dragstart.force",Qr).on("drag.force",t).on("dragend.force",nu)),arguments.length?void this.on("mouseover.force",tu).on("mouseout.force",eu).call(r):r},oa.rebind(l,c,"on")};var ml=20,yl=1,Ml=1/0;oa.layout.hierarchy=function(){function n(u){var i,a=[u],o=[];for(u.depth=0;null!=(i=a.pop());)if(o.push(i),(c=e.call(n,i,i.depth))&&(l=c.length)){for(var l,c,s;--l>=0;)a.push(s=c[l]),s.parent=i,s.depth=i.depth+1;r&&(i.value=0),i.children=c}else r&&(i.value=+r.call(n,i,i.depth)||0),delete i.children;return au(u,function(n){var e,u;t&&(e=n.children)&&e.sort(t),r&&(u=n.parent)&&(u.value+=n.value)}),o}var t=cu,e=ou,r=lu;return n.sort=function(e){return arguments.length?(t=e,n):t},n.children=function(t){return arguments.length?(e=t,n):e},n.value=function(t){return arguments.length?(r=t,n):r},n.revalue=function(t){return r&&(iu(t,function(n){n.children&&(n.value=0)}),au(t,function(t){var e;t.children||(t.value=+r.call(n,t,t.depth)||0),(e=t.parent)&&(e.value+=t.value)})),t},n},oa.layout.partition=function(){function n(t,e,r,u){var i=t.children;if(t.x=e,t.y=t.depth*u,t.dx=r,t.dy=u,i&&(a=i.length)){var a,o,l,c=-1;for(r=t.value?r/t.value:0;++cf?-1:1),p=oa.sum(c),v=p?(f-l*g)/p:0,d=oa.range(l),m=[];return null!=e&&d.sort(e===xl?function(n,t){return c[t]-c[n]}:function(n,t){return e(a[n],a[t])}),d.forEach(function(n){m[n]={data:a[n],value:o=c[n],startAngle:s,endAngle:s+=o*v+g,padAngle:h}}),m}var t=Number,e=xl,r=0,u=Ua,i=0;return n.value=function(e){return arguments.length?(t=e,n):t},n.sort=function(t){return arguments.length?(e=t,n):e},n.startAngle=function(t){return arguments.length?(r=t,n):r},n.endAngle=function(t){return arguments.length?(u=t,n):u},n.padAngle=function(t){return arguments.length?(i=t,n):i},n};var xl={};oa.layout.stack=function(){function n(o,l){if(!(h=o.length))return o;var c=o.map(function(e,r){return t.call(n,e,r)}),s=c.map(function(t){return t.map(function(t,e){return[i.call(n,t,e),a.call(n,t,e)]})}),f=e.call(n,s,l);c=oa.permute(c,f),s=oa.permute(s,f);var h,g,p,v,d=r.call(n,s,l),m=c[0].length;for(p=0;m>p;++p)for(u.call(n,c[0][p],v=d[p],s[0][p][1]),g=1;h>g;++g)u.call(n,c[g][p],v+=s[g-1][p][1],s[g][p][1]);return o}var t=y,e=pu,r=vu,u=gu,i=fu,a=hu;return n.values=function(e){return arguments.length?(t=e,n):t},n.order=function(t){return arguments.length?(e="function"==typeof t?t:bl.get(t)||pu,n):e},n.offset=function(t){return arguments.length?(r="function"==typeof t?t:_l.get(t)||vu,n):r},n.x=function(t){return arguments.length?(i=t,n):i},n.y=function(t){return arguments.length?(a=t,n):a},n.out=function(t){return arguments.length?(u=t,n):u},n};var bl=oa.map({"inside-out":function(n){var t,e,r=n.length,u=n.map(du),i=n.map(mu),a=oa.range(r).sort(function(n,t){return u[n]-u[t]}),o=0,l=0,c=[],s=[];for(t=0;r>t;++t)e=a[t],l>o?(o+=i[e],c.push(e)):(l+=i[e],s.push(e));return s.reverse().concat(c)},reverse:function(n){return oa.range(n.length).reverse()},"default":pu}),_l=oa.map({silhouette:function(n){var t,e,r,u=n.length,i=n[0].length,a=[],o=0,l=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];r>o&&(o=r),a.push(r)}for(e=0;i>e;++e)l[e]=(o-a[e])/2;return l},wiggle:function(n){var t,e,r,u,i,a,o,l,c,s=n.length,f=n[0],h=f.length,g=[];for(g[0]=l=c=0,e=1;h>e;++e){for(t=0,u=0;s>t;++t)u+=n[t][e][1];for(t=0,i=0,o=f[e][0]-f[e-1][0];s>t;++t){for(r=0,a=(n[t][e][1]-n[t][e-1][1])/(2*o);t>r;++r)a+=(n[r][e][1]-n[r][e-1][1])/o;i+=a*n[t][e][1]}g[e]=l-=u?i/u*o:0,c>l&&(c=l)}for(e=0;h>e;++e)g[e]-=c;return g},expand:function(n){var t,e,r,u=n.length,i=n[0].length,a=1/u,o=[];for(e=0;i>e;++e){for(t=0,r=0;u>t;t++)r+=n[t][e][1];if(r)for(t=0;u>t;t++)n[t][e][1]/=r;else for(t=0;u>t;t++)n[t][e][1]=a}for(e=0;i>e;++e)o[e]=0;return o},zero:vu});oa.layout.histogram=function(){function n(n,i){for(var a,o,l=[],c=n.map(e,this),s=r.call(this,c,i),f=u.call(this,s,c,i),i=-1,h=c.length,g=f.length-1,p=t?1:1/h;++i