RailsのResourcesでNestしてshallow: trueしたresourcesをAngularJSの$resourceで扱う
例えばこういう感じのresourcesを定義したとする。
APIの例。newとeditは省いた。
# routes.rb namespace :api, defaults: { format: :json } do resources :reports, shallow: true do resources :comments, except: [:new, :edit] end end
定義されるルーティングはこうなる。reportsのは話に関係ないので省略
GET /api/reports/:report_id/comments(.:format) api/comments#index {:format=>:json} POST /api/reports/:report_id/comments(.:format) api/comments#create {:format=>:json} GET /api/comments/:id(.:format) api/comments#show {:format=>:json} PATCH /api/comments/:id(.:format) api/comments#update {:format=>:json} PUT /api/comments/:id(.:format) api/comments#update {:format=>:json} DELETE /api/comments/:id(.:format) api/comments#destroy {:format=>:json}
Railsではよくあるパターンだと思う。
このコメントのresourceをAngularJSの$resourceで使いたい。
$resourceでよく見かけるサンプルだとシンプルなものが多い.
こういったことは出来ないんだろうかと、AngularJSのリファレンスで$resourceの項目を見てみたら、ちゃんとやり方が書いてあった。
url – {string} – action specific url override. The url templating is supported just like for the resource-level urls.
$resourceの第3引数のactionsでurlをoverrideすればよいと。
やってみた。
下記はqueryとかsaveとか自動のやつは無視してRails的なactionを定義
# Comment.coffee 'use strict' angular.module('myApp') .factory 'Comment', ($resource) -> $resource("/api/comments/:commentId", {commentId: "@id", reportId: "@report_id"}, { index: {method: "GET", url: "/api/reports/:reportId/comments", isArray: true} create: {method: "POST", url: "/api/reports/:reportId/comments"} update: {method: "PATCH",params: {commentId: "@id"}} show: { method: "GET",params: {commentId: "@id"}} destroy: {method: "DELETE",params: {commentId: "@id"}} })
使う例
# sample.coffee angular.module('myApp') .controller "SampleCtrl", ($scope, $stateParams, Comment) -> $scope.comments = Comment.index(reportId: $stateParams.reportId) $scope.comment = new Comment(report_id: $stateParams.reportId) $scope.save = -> $scope.comment.$create((comment)-> # do something
上記の場合だと、report_idとreportIdでもにょってしまうので、サービス側でもう少しラップしてオブジェクトの引数ではなく、パラメータで渡した方が幸せになれるかもしれない。
RailsでつくるAPIサーバのドキュメントを自動生成してくれるAutodocを使っていて気をつけること
Autodocとはなんぞやという方はこちらの記事を参照。 公式はこちら
で、大変便利なAutodocだけど、注意点が2つ。
まず一点目はRspc3で動かない点。プルリクが上がってるが、取り込まれてない。
なので、forkして使ってる人が結構いる感じ。
2点目、通信が発生しているところでマークダウンが生成されるので、shared_exampleとか使って、そこで実際の通信が発生するようなものは、ドキュメントが全てshared_exampleがあるファイル名で生成されてしまう。
具体的に言うと、例えばこちらの記事のようなヘルパーメソッドを用意して、これでAUTODOC=1でAPIのテストを全て行った場合、shared_exmpleを呼び出している個別のspecファイルではなく、doc/support/api_helper.mdというドキュメントが生成されてしまう。
こうなるのは仕方がない気がするので、autodocを使ってドキュメントを生成する場合は、Autodocで記録する個別のexampleの中でgetやpostを投げる感じで。
Rails4をJSON APIとして構築していてCreateのAPIに関連のID一覧をparameterとしてPOSTしてはまった
前提
1.Railsのwrap_parametersはJSONをRailsへ送った時にルート要素を省いてくれる。というか、省いて送ってもよしなにwrapしてくれる。
Railsのwrap_parametersは何をしてくれるのか?
2.has_manyを定義するとrelation_ids,relation_ids=というメソッドが使えるようになる
#group.rb Class Group has_many :groups_users has_many :users, through: :groups_users end
#user.rb class User has_many :groups_users has_many :groups, through: :groups_users end
#irb group = Group.first group.user_ids # =>[1,2,3] group.user_ids=[2,3,4]
3.Railsのstrong_parametersは許可されたパラメータ以外を取り除く
#groups_controller.rb class GroupsController < ApplicationController def create @group = Group.new(group_params) # 省略 end private def group_params params.require(:group).permit(:name, user_ids: []) end end
問題
このgroups#createに対して以下のようなJSONを送付したところ、groupモデルは作成されたが、userとの関連は作成されなかった。
// json { name: "新規グループ", user_ids: [1,2,3] }
user_idsがstrong_parametesに書いたにもかかわらず弾かれる。
実際のparamsを見るとこんな感じ
Parameters: {"name"=>"新規グループ", "user_ids"=>[1,2,3], "group"=>{"name"=>"新規グループ"}}
調べるとどうもnested_attributes_forも引っかかるようだ。
解決
strong_paramtersのREADMEに書いてあった。
以下のようにrequire(:group)を取り除く
# groups_controller.rb class GroupsController < ApplicationController # 省略 private def group_params params.permit(:name, user_ids: []) end end
これで、wrap_parametersもきき、groupとuserの関連も作成される。
Yeomanのgenerator-angularとRailsの組み合わせでの開発環境構築
grunt-connect-proxyを使って、rails server と、grunt serve の2つを叩いてLiveReloadで開発出来るようにしようという話。
あとフロント側をビルドするとRailsのpublicディレクトリに静的ファイルとして配備するように。
RailsのAsset PipelineにAngularJSを載せるんじゃなくて、サーバーサイド(Rails)とクライアントサイド(AngularJS)を分離しての開発の話。
こういう事↓
理想的な Rails, AngularJS 環境の構築 - ボクココ
前提
Railsアプリ作成して、apiのnamespaceでAPIを作成済み、Railsアプリのルートにngappとかディレクトリ作って、yo angular --coffee でフロントエンド部を生成済みとする。*1
作業
% cd ngapp
% npm install -D grunt-connect-proxy
# Gruntfile.coffee "use strict" proxySnippet = require('grunt-connect-proxy/lib/utils').proxyRequest # ↑追加 module.exports = (grunt) -> require("load-grunt-tasks")(grunt) require("time-grunt")(grunt) # Configurable paths for the application appConfig = app: require("./bower.json").appPath or "app" dist: "../public" # ←修正. railsのpublicをdistに # Define the configuration for all the tasks grunt.initConfig # Project settings yeoman: appConfig # ................................省略................................. connect: options: port: 9000 # Change this to '0.0.0.0' to access the server from outside. hostname: "localhost" livereload: 35729 livereload: options: open: true middleware: (connect) -> [ proxySnippet #<- 追加 connect.static(".tmp") connect().use("/bower_components", connect.static("./bower_components")) connect.static(appConfig.app) ] proxies: [ #<- proxies: [...] 追加 context: '/api' host: 'localhost' port: '3000' ] # ................................省略................................. grunt.registerTask "serve", "Compile then start a connect web server", (target) -> if target is "dist" return grunt.task.run([ "build" "connect:dist:keepalive" ]) grunt.task.run [ "clean:server" "wiredep" "concurrent:server" "configureProxies" #<- 追加 "autoprefixer" "connect:livereload" "watch" ] # ................................省略.................................
% bundle exec rails server
% grunt serve
これで、localhost:9000/にアクセスするとフロント側、localhost:9000/apiがRailsのAPI側に*2。
APIとは別に、管理系とかを普通にRailsアプリとして作って同じプロジェクト上にある場合は、Railsのお作法に則り、localhost:3000/下にアクセスでいいと思う。
ビルドは,--forceを忘れずに
grunt build --force
2014/8/23追記
Vagrantの場合↓ Yeomanのgenerator-angularで作ったプロジェクトをVagrant環境で開発する場合の設定 - 仙台 Ruby Vim JavaScript 社長
参考
Deviseでnamespace使う場合はdevise_for :users, path: :adminとかしとけという話
メモ。
Devise使用時にroutesでadminとかのnamespaceにdevise_forを入れると,Devise::SessionsController等で authenticate_user! が authenticate_admin_user! とかなってしまう*1
# config/routes.rb namespace :admin do devise_for :users, controllers: { sessions: "admin/users/sessions", ...... } end
devise_for with namespace generate wrong methods · Issue #412 · plataformatec/devise · GitHub
こう書く。
# config/routes.rb devise_for :users, path: :admin, controllers: { sessions: "admin/users/sessions", ...... }
今回は諸事情があってDevise使ってるけど、認証はRails提供のsecure_passwordとか使って、自前で書いたほうがはまらなくていいと思う。大分楽に書けるようになったし。Deviseはカスタマイズしようとすると面倒くさい。
*1:注:カスタムしたコントローラーに任意のアクションを突っ込んでしまっているからです。