- 投稿日:2019-11-29T23:48:44+09:00
Twitterでのコード投稿の見栄えどうにかならんのと思った話
たぶん、長い投稿
きっかけ(こんな呟きを見かけた
ソースコードをツイートするときに
— えるは個人えんじにゃー(喪中) (@ellnore_pad_267) November 2, 2019<br>source code<br>
ってやってマークダウンみたいに引用文にして欲しい。
ここはもうURLとかハッシュタグとかも全部エスケープして欲しい。出来たもの
作成の過程で収穫物
- Rails5.2での追加分(Active Record Storage含む
- Twitter Login方法と仕組み、そのたTwitterあれこれ
- JSの基礎(getElementByIdやsetAttribute、文字カウントなど
- AWS S3の使い方
- XSS対策
未だ残る改修すべき箇所
- 検索結果画面のリダイレクトエラー(多分route.rbの書き順番由来
- js辺りのエラー(動いてるけど、consoleではjs/mapのルーティングが何とか
- スマホで
タグ等を打つの面倒なので、なにか投稿補助ボタンでも- 本来の目的をよく考えたら、マークダウンの方は不要なのでは。
作成要件
- マークダウン投稿、シンタックスハイライト
- gem: redcarpet, rouge(結局syntax-hightlightだけは反映されないまま
- 投稿から画像生成
- AWS S3にog:image用の画像を保存
作成の流れ:予定
- rails new codr, git init, heroku create、Active Storage
- AWS S3あれこれ
- twitter登録、ログイン機能作成
開発環境
- vm : Linux Ubuntu (virtualbox + vagrant)
- Ruby 2.5.1p57
- Rails 5.2.3
- Postgresql
実作業: アプリ作成、諸準備
rails new codr -d postgresql # DB設定等は割愛Gem
今回は公開にまで至る予定なので、railsやdeviseの日本語化等も。が、想定ユーザはエンジニアだしと思い、殆ど英語になった。
Gemfilegem 'mini_racer' # uncomment gem 'rails-i18n' # japanize # authetication gem 'devise' # login gem 'omniauth' # SNS login gem 'omniauth-twitter' # twitter login gem 'devise-i18n' # japanize devise gem 'devise-i18n-views' gem 'redcarpet' # for markdown gem 'rouge' # for syntax highlight gem 'meta-tags' gem 'aws-sdk-s3' # for aws s3kpumuk/meta-tags:Search Engine Optimization (SEO) for Ruby on Rails applications.は割愛。
gitignore => rails.credentials.yml
当初は.
gitignore
とgem 'dotenv'等を使っていた。が、作成途中でRails5.2からのrails.credentials.ymlを知り、利用した。rails.credentials.ymlは暗号化されており、なお、復号化には
/config/master.key`を利用。irb# editor setting EDITOR="vi" bin/rails credentials:edit # edit credentials.yml rails credentials:edit # show credential.yml rails credentials.yml:show # herokuにmaster.keyを環境変数として指定 heroku config:set ENV_VAR="環境変数" --app "アプリ名" # 追加した変数を使用するには Rails.application.credentials.dig(:twitter, :API_Key)rails gあれこれ
# devise # install devise rails g devise:install rails g devise User name:String # Add Admin column to User rails g migration AddAdminToUsers # add setting at /db/migrate/20191103141531_add_admin_to_users.rb add_column :users, :admin, :boolean, default: false # add views and controllers to modify devise rails g devise:controllers users rails g devise:views users # japanize # add at /config/application.rb config.i18n.default_locale = :ja => create /config/locale/devise.view.ja.yml# scaffold post rails g scaffold Post user:references name:string content:text date:datetimeActive Record Associations関連付け
/app/model/user.rbhas_many :posts/app/model/post.rbbelongs_to :user投稿関連
マークダウン投稿
基本:
Redcarpet::Markdown.new(renderer, extensions = {}).render(@post.content)
オプションやXSS対策等を追加したく、helperメソッドを作成した。app/helpers/posts_helper.rbModule PostsHelper require 'rouge/plugins/redcarpet' class RougeRedcarpetRenderer < Redcarpet::Render::HTML include Rouge::Plugins::Redcarpet def header(text, level) # #や##等がh2、h3となるようにした。 level += 1 "<h#{level}>#{text}</h#{level}>" end end def markdown(text) render_options = { filter_html: true, # do not allow any user-inputted HTML in the output. hard_wrap: true, } extensions = { autolink: true, # <>で囲まれていない時は、リンクとして認識しない fenced_code_blocks: true, # ```\n ```内をコード部分と見做す lax_spacing: true, no_intra_emphasis: true, strikethrough: true, superscript: true, tables: false, # テーブルを認識しない highlight: true, disable_indented_code_blocks: true, space_after_headers: false # #の後にスペースが無くても、h1等とする。 } renderer = RougeRedcarpetRenderer.new(render_options) Redcarpet::Markdown.new(renderer, extensions).render(text).html_safe end endhtml_safe => sanitize
html_safeではXSS対策としては駄目と知った。名前詐欺である。
sanitizeヘルパーを使用した。ホワイトリスト方式。要参照app/views/posts/index.html.erb# sanitize(html, options = {}) <div id="capture" class="content"> <%= sanitize(markdown(@post.content), tags: %w(div img h1 h2 h3 h4 h5 strong em a p pre code ), attributes: %w(class href)) %> </div>投稿内容のデータ化、AWSへの画像保存
最初はTwitterAPIを利用して、投稿から作成、DBに直接保存した画像でTwitter投稿しようとした。だが、Herokuでは画像が保持されない事、TwitterAPIの変更などいろいろ面倒なことが発生したので、最終的には画像をAWS S3に保存し、og:imageに添付する形を取った。
- Webアプリ内で通常投稿
- showページ表示(同時にhtml2canvasでBase64としてデータ取得、hidden_fieldに格納
- Tweetボタン押す(Postされ、postモデル内でbase64をデコード
- Active Storageを通して、AWS S3に保存
Active Storage
Rail5.2からの機能で、今までのcarrievaveやpaperclip等を使わずに、クラウドストレージ等へのアップロードが容易になる。今回はAWS S3を使った。
irb# set up rails active_storage:install # 今回は画像が紐づくPostテーブルが既にあるので、不要 # rails g resource comment content:text rails db:migrateapp/models/post.rbclass Post < ApplicationRecord # 今回は1つの投稿につき、1枚の画像なので。複数なら => has_many_attached :prtscs has_one_attached :prtsc endapp/config/enviroments/# ファイル保存先変更 # development.rb config.active_storage.service = :local # production.rb config.active_storage.service = :amazon
rails credentials:edit
でAWSアクセスキーとシークレットキーを追加。config/credentials.yml.encaws: access_key_id: secret_access_key:config/storage.ymltest: service: Disk root: <%= Rails.root.join("tmp/storage") %> local: service: Disk root: <%= Rails.root.join("storage") %> amazon: service: S3 access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> region: ap-northeast-1 bucket: codr0Gemfile# gemが必要 gem 'aws-sdk-s3', require: false # 今回は不要だったので、入れず。 gem 'mini_magick'html2canvas
参考:htmlを画像化する方法(html2canvasの使い方)
jsはProgateレベルだったので、DOM操作は初めてで、なんか楽しかったぞ。
- Tweetボタン押下時に、画像をPostするためのフォーム、hidden_fieldを用意
html2canvas.js
をapp/assets/javascripts
ディレクトリ配下に保存。- html上に置くscriptコードを改修
app/views/posts/show.html.erb<%= form_with(model: @post, local: true) do |form| %> <%= form.hidden_field :id, value: @post.id %> <%= form.hidden_field :prtsc, value: "" %> # idはpost_prtscになる。 <%= form.submit "Post", class:"btn btn-outline-dark", id:"tweet", value:"tweet" %> <% end %>app/views/layouts/application.html.erb<script type="text/javascript"> html2canvas(document.querySelector("#capture"),{scale:1, width:600}).then(canvas => { var base64 = canvas.toDataURL('image/jpeg', 1.0); document.getElementById('post_prtsc').setAttribute('value', base64); }); </script>Base64デコード
大学で画像処理していたとはいえ、 Base64とは?Blobとは?となり、良い機会だった。
app/models/post.rbattr_accessor :img def parse_base64(img) if img.present? # data:image/jpeg;base64,/9j/4AAQSkZJRgABA・・・から/9j/4AA以降を選択取得 content = img.split(',')[1] # 今回は、ユーザによる画像アップロード投稿ではなく、拡張子が決まっている filename = Time.zone.now.to_s + '.jpg' decoded_data = Base64.decode64(content) # String.IO.newにより、アプリ内に一時ファイルを作成しなくて済む prtsc.attach(io: StringIO.new(decoded_data), filename: filename) end endあとはposts_controllerで、paramsから受け取ったBase64データを上の
parse_base64(img)
で変換し、保存すれば完了。AWS S3
AWS上での登録、設定、バケット作成等は割愛。
Tweet button
公式で生成されるTweetボタンのURLを利用し、押下時にwindow.openでTweet投稿ページを開くようにした。rubyonrailsで用意した変数をjsに渡す
gem 'gon'
も考えたが、見送った。app/views/layouts/application.html.erb<script> var base = 'https://twitter.com/intent/tweet?url='; var pageUrl = 'https://codr0.herokuapp.com/posts/' + document.getElementById('post_id').value; var option = '&button_hashtag=Codr0&ref_src=twsrc%5Etfw'; var href = base + pageUrl + option; var twit = document.getElementById('tweet'); twit.addEventListener('click', function() { window.open( href ); }); </script>og:imageに画像添付
なお、headのmeta情報セットには、
gem 'meta-tags'
を使用。参照 : kpumuk/meta-tagsservice_url()とurl_for()
基本的にはどちらも、ActiveStorageに保存したデータのUrlを取得するメソッドの様だ。
どちらもセキュリティの為にリンクの有効期限が短いみたいだが、違いが分からなかった。今回はTweetボタン押下し、Tweetした際にog:imageとして表示されればいい。app/views/posts/show.html.erb# 画像がActive StorageでAWS S3に保存されて入れば <% if @post.prtsc.attached? %> <% set_meta_tags og:{image: @post.prtsc.service_url} %> <% end %>Twitterログイン
TwitterDeveloperAccountが必要。割愛。
なお、omniauthは脆弱性が見つかっており、githubの方でもアラートが来るのだが、パッチが無いのだが。クックパッドの人が対処してくれたので、感謝したい。
app/models/user.rb# 参考ページと同じ基礎的な所は割愛する。 class User < ApplicationRecord def self.from_omniauth(auth) find_or_create_by!(provider: auth['provider'], uid: auth['uid']) do |user| # 一部割愛 user.username = auth['info']['nickname'] # SNS登録時は、ダミーメールを登録 user.email = User.dummy_email(auth) end end # SNS登録(providerが存在する)時は、パスワード要求をしない def password_required? super && provider.blank? end def self.new_with_session(params, session) if session['devise.user_attributes'] new(session['devise.user_attributes']) do |user| user.attributes = params end else super end end private def self.dummy_email(auth) "#{auth.uid}-#{auth.provider}@example.com" end endTwitterのニックネームが取得できるようになったので、元からあるUserのnameテーブルは削除した。
css
今回はBootstrapを部分的に使用した。cssの優先順位など収穫があった。
改修(加筆
メディアクエリ
想定ユーザは殆どスマホなのに、PCで作成し、CSSをPCの見た目でやってた。折角SCSSでやってるので、変数を利用した。
app/assets/stylesheets/scaffold.scss# ディスプレイサイズが680pxまでなら。 $tab: 680px; @mixin tab { @media (max-width: ($tab)) { @content; } } // .box { // @include tab { // background-color: blue; // }; // }最後に
gist等がコードスクショをog:imageで表示してくれたら全て済むんじゃと思った。
因みにもう1段階先のWebアプリを考えてあるけど、たぶんjsの知識が足りないので、今は無理。
転職活動中の無職です。働きたい。。。。
- 投稿日:2019-11-29T21:19:42+09:00
HamlをRuby on Railsアプリケーションに導入する方法
Hamlとは
Hamlとは、HTMLを簡潔かつ簡単に記述できるマークアップ言語。
Railsで使用する場合の手順
1. Hamlを導入
Gemfileの一番下にコードを記述すると、すべての環境でhamlが使用できる。
記述後は、bundle installを忘れずに 。Gemfilegem 'haml-rails'2. erbファイルをHamlに変換
rails newにより、erbファイルがすでに作成されているので
拡張子がerbのファイルをhamlに変換する。一括変換する方法
ターミナルから以下のコマンドを実行。
$ rails haml:erb2hamlここで、
Would you like to delete the original .erb files? (This is not recommended unless you are under version control.) (y/n)と聞かれたら、状況に応じてy/nを選択する。
元のerbファイルを削除して構わなければ yを選択しよう。
- 投稿日:2019-11-29T19:41:00+09:00
rails-tutorial第2章
scaffold
MVCアーキテクチャではmodel,view,controlerが組み合わさって一つのリソースと考える。scaffoldを使うとリソース単位で自動生成してくれる。
scaffoldをしてサーバーを立ち上げると、その時点からuserリソースを扱える。/users/newとかできる。migration file
これはrailsとDBが全く別物だから、migration fileに書いて〜を保存するスペース作ってくれない?とお願いをしなければいけない。なので、rails generate scaffold User name:string email:stringなどの指示を出した後はrails db:migrateをしないとDBにリソースを保存することができない。ちなみに、rails db:migrateをせずにrails sをすると、 migration pending(DBに何かお願いすることあるんじゃないの?)というエラーを出してくれる。rails db:migrateはべきとうせいである。つまり何度実行しても追加したmigration fileがなければ何も起こらない。
resources
これはルーティングのパッケージ版。本来 /users => user#indexなど基本のルーティングがパッケージングされてる。
actionのデフォルトview
例えばindexアクションを呼び出した時、renderなどのviewを呼び出す指示が書いてないことがほとんど。その場合は、アクション名からapp/views/users/index.html.erbが呼び出される仕組みになっている。
後半戦
bundle exec
このコマンドはgem fileを読み込んだ上でexec以下のコマンドを実行するというもの。 ex)bundle exec rails --version
テーブルの関連付け
class User < ApplicationRecord has_many :microposts endclass Micropost < ApplicationRecord belongs_to :user validates :content, length: { maximum: 140 } endこれによって、User.first.micropostsなどが実行できる。
そもそもなんで上記のコマンドでテーブルが結合される?
これはrailsのデフォルトによるもの。
micropostsテーブルにはuser_idというカラムがある。
model名_idというカラムがあり、結合すると、model名(ここではusersテーブル)のidカラムと自動的に結合してくれるから。validation
class Micropost < ApplicationRecord validates :content, length: { maximum: 140 } endこれによって投稿の文字数を140文字に制限することができる。
heroku注意点
上記のようにテーブルを作った状態でherokuにアップしようとすると、エラーになってしまう。
それはmigrationfileが開発用には適用されたが、本番環境では適応されていないから。開発環境用のDBと本番環境用のDBが違うことから起きる。そのため、 heroku run rails db:migrate としてあげる。
つまり、migrationfileを一つでも作ったら、git push heroku masterをした後に上記のコマンドを実行しなければいけないということ。
また、DBは違えど、Active RecordによってSQLが適切に書かれるのでそこを気にする必要はない。
- 投稿日:2019-11-29T19:13:26+09:00
【Rails】Docker+Rails環境でpryの`edit`コマンドが使えなかったので、使えるようにした
はじめに
DockerとRailsを使った開発環境でpryの
edit
コマンドが効かなかったので対処してみたときの記録です。※Docker環境下での話です。ローカルでは普通に動作しましたので、割愛します。
この記事が役に立つ方
- あれ?なんか
edit
コマンド効かなくね?と思っている方この記事のメリット
- Docker環境下でpryの
edit
コマンドが使えるようになる環境
- macOS Catalina 10.15.1
- zsh: 5.7.1
- Ruby: 2.6.5
- Rails: 5.2.3
- Docker: 19.03.5
- docker-compose: 1.24.1
pryの
edit
コマンドとは?以下、公式のWikiです。
Editor integration · pry/pry Wiki · GitHubThe edit command is used to invoke your default editor. This command will load your file once you have finished editing it (unless you pass the -n or --no-reload flag to edit).
pryの画面でそのままデフォルトのエディタを開き、ファイルの編集が出来るコマンドです。
これまでpryの画面上からファイルをいじったりはしていなかったのですが、デバッグ効率が上がるかなと思ったので導入します。
【事前準備】
こちらのドキュメントに従ってDocker環境下でgem pry-railsを使うための準備は済ませておきました。
Using pry-rails with Docker · GitHub
上記ドキュメントの流れは以下の通りです。
docker-compose.ymlapp: tty: true stdin_open: true上記設定を追加。
↓Gemfilegroup: :development gem 'pry-rails' endgemを追加。
↓docker-compose buildビルド。
↓docker-compose up起動。
↓docker psRailsアプリの
CONTAINER ID
かNAMES
を確認
↓docker attach `CONTAINER ID`か`NAMES`↓
binding.pry ※好きなところでこれで普通にpryでデバッグ可能な状態になります。
とりあえず
edit
コマンド使ってみようとする※Docker環境下です。
とりあえず
edit
コマンドを叩いてみます。[1] pry(main)> edit Error: Please set Pry.config.editor or export $VISUAL or $EDITORするとエラー発生。
Pry.config.editor
か、環境変数$VISUAL
か$EDITOR
を設定してねという内容。先程のWikiの、Setting the default editorのくだりに設定方法が記載されていたので、その通りに進めてみます。
1.
~/.pryrc
を作成・設定pryの設定ファイルは
.pryrc
です。
今回はなかったので作成しました。以下設定方法についてです。
Editor integration · pry/pry Wiki · GitHub今回エディタはvimを使いたかったので、以下のように設定。
~/.pryrcPry.config.editor = proc { |file, line| "vim #{file}:#{line}" }該当のファイルと行に飛んでくれる仕様に例にならってみましたが、うまくいきません。
ローカルの設定ファイルは読み込まれないんですね。
2.Railsアプリのディレクトリ内に
.pryrc
を配置する。下記記事を参照し、Railsアプリのルートディレクトリに
.pryrc
を配置してみることにしました。docker-compose上のRailsのデバッグを行う - My External Storage
./.pryrcPry.config.editor = proc { |file, line| "vim #{file}:#{line}" }※設定は先程と同様です。
もう一回試します。
[1] pry(main)> edit Error: `vim ファイル名:行数` gave exit status: 127別のエラー発生。
惜しい!コマンド自体は正しそうに見えますが、127
なるエラーが。何これ?と思って調べると、
「コマンドが見つからない、もしくはタイポ」
らしいと判明。...ということは、そもそも
vim
がない?3.
vim
をインストールするなければいくらコマンドが正しくても、どうしようもないですよね。
調べると、こちらの記事を発見しました。
Docker — docker コンテナの中で vim が使えない場合 - QiitaDockerfileapt-get install vim上記を追記。
↓docker-compose build↓
docker-compose up↓
rails c
などでpry起動。[1] pry(main)> edit #=>vim起動無事起動!
おわりに
最後まで読んで頂きありがとうございました
vimがないという環境もあるのかとびっくりしましたが、ローカルとDocker環境の境目を意識するいい経験になりました
これでデバッグ効率が上がりそうです
参考にさせて頂いたサイト(いつもありがとうございます)
- 投稿日:2019-11-29T16:24:49+09:00
railsのバージョン変更する
cloneしたgemfileのバージョンにrailsのバージョンを合わせたい
該当のバージョンをインスール
$ gem install -v 5.2.1 railsエラーが起きる
gem install -v 5.2.1 rails ERROR: Error installing rails: The last version of sprockets (>= 3.0.0) to support your Ruby & RubyGems was 3.7.2. Try installing it with `gem install sprockets -v 3.7.2` and then running the current command again sprockets requires Ruby version >= 2.5.0. The current ruby version is 2.4.2.198.rubyのバージョンを変更したので、それに合わせて
gemのアップデート $ gem update --systems 新しくインストールしたrubyの環境にbundleを入れる $ gem install bundlerそして先ほどのエラー文を読むと
gem install sprockets -v 3.7.2
してください。となっていたのでこれを実行する。
もう一度
$ gem install -v 5.2.1 rails
- 投稿日:2019-11-29T16:23:38+09:00
#Rails + rspec + rake タスクで環境変数を利用してタイムゾーンを指定したテストをする例 ( specify local timezone test with rake )
rake
namespace :foo do task bar: :environment do |task| date = if ENV['ON'].present? Date.parse(ENV['ON']) else Time.current.in_time_zone('Tokyo').to_date.yesterday end Alice.run!(on: date) end endspec
require "rails_helper" require "rake" describe 'foo' do before(:all) do @rake = Rake::Application.new Rake.application = @rake Rake.application.rake_require('bar', [Rails.root.join('lib', 'tasks', 'foo')]) Rake::Task.define_task(:environment) end before(:each) do @rake[task].reenable end describe do let(:task) { 'foo:bar' } context 'when now is beggining of day at JST' do before do travel_to '2020-01-01 00:00:00'.in_time_zone('Tokyo') end it do expect(Alice).to receive(:run!).with(on: Date.new(2019, 12, 31)) @rake[task].invoke end end context 'when now is end of day at JST' do before do travel_to '2020-01-01 23:59:59'.in_time_zone('Tokyo') end it do expect(Alice).to receive(:run!).with(on: Date.new(2019, 12, 31)) @rake[task].invoke end end context 'when specify target date' do before do travel_to '2020-01-01 00:00:00'.in_time_zone('Tokyo') allow(ENV).to receive(:[]).with('ON').and_return('2020-02-01') end it do expect(Alice).to receive(:run!).with(on: Date.new(2020, 02, 01)) @rake[task].invoke end end end endOriginal by Github issue
- 投稿日:2019-11-29T15:30:54+09:00
【Ruby on Rails】開発環境構築(MacOS)
MacOSの状態を確認
Catalina以降の場合とMojave以前の場合で環境構築の設定が異なります。
OSがMojave以前の場合の環境構築
❶Command Line Toolsを用意
Command Line ToolsはWebアプリケーション開発に必要なソフトウェアをダウンロードするために必要な機能です。
$ xcode-select --install❷Homebrewを用意
$ cd #ホームディレクトリに移動 $ pwd #ホームディレクトリにいるかどうか確認 $ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" #コマンドを実行※処理に時間がかかる可能性のある操作です。
「
Press RETURN to continue or any other key to abort
」と表示されたら、「続けるにはエンターキーを、やめるにはそれ以外の入力をしてください
」という意味ですので、エンターキーを入力します。「
Password:
」と表示されたら、PCを開く際に求められるパスワードを入力します。ターミナル上で文字は表示されませんが、間違いなく入力はされています。パスワードを入力し終わったらエンターキーを押します。$ brew -v #Homebrewがインストールされているか確認 Homebrew 1.8.0$ brew update #Homebrewを最新状態にする$ sudo chown -R `whoami`:admin /usr/local/bin #Homebrewの権限を変更❸新しいバージョンのRubyをインストール
$ brew install rbenv ruby-build #Homebrewを用いてrbenvとruby-buildをインストール$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile #PCにおけるどこのディレクトリからも使用できるようにする$ source ~/.bash_profile #bash_profileの変更を反映$ brew install readline #readlineをインストール$ brew link readline --force #どこからも使用できるように# rbenvを利用してRubyをインストール $ RUBY_CONFIGURE_OPTS="--with-readline-dir=$(brew --prefix readline)" $ rbenv install 2.5.1$ rbenv global 2.5.1 #利用するRubyのバージョンを指定$ rbenv rehash #rbenvを読み込んで変更を反映$ ruby -v #Rubyのバージョンを確認先ほどインストールした2.5.1であることが表示されれば完了です。
❹MySQLを用意
MySQLは、Webアプリケーションにおけるデータを蓄積する場所のこと。
# MySQLのインストール $ brew install mysql@5.6 $ brew reinstall https://raw.githubusercontent.com/Homebrew/homebrew-core/f171f1c74/Formula/mysql@5.6.rb※処理に時間がかかる可能性のある操作です。
# MySQLの自動起動設定 $ mkdir ~/Library/LaunchAgents $ ln -sfv /usr/local/opt/mysql\@5.6/*.plist ~/Library/LaunchAgents $ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql\@5.6.plist# mysqlのコマンドをどこからでも実行できるようにする $ echo 'export PATH="/usr/local/opt/mysql@5.6/bin:$PATH"' >> ~/.bash_profile $ source ~/.bash_profile# mysqlのコマンドが打てるか確認する $ which mysql# 以下のように表示されれば成功 /usr/local/opt/mysql@5.6/bin/mysql❺Railsを用意
Railsというフレームワークを用いて、Rubyでアプリケーションを作成します。
$ gem install bundler #bundlerをインストール$ gem install rails --version='5.2.3' #Railsをインストール※処理に時間がかかる可能性のある操作です。
$ rbenv rehash #rbenvを再読み込みしておく$ rails -v #Railsが導入できたか確認Mojave以前の場合は、これにてWebアプリケーション開発のための環境構築は完了です。
OSがCatalina以前の場合の環境構築
# zshをデフォルトに設定 $ chsh -s /bin/zsh# ログインシェルを表示 $ echo $SHELL# 以下のように表示されれば成功 /bin/zshもし
echo $SHELL
コマンドで、/bin/zsh
と表示されなかった方は、一度ターミナルを閉じて再度開いた上でもう一度echo $SHELL
コマンドを実行します。(“$
”が“%
”になっている可能性もあります)❶Command Line Toolsを用意
$ xcode-select --install #Command Line Toolsをインストール❸Homebrewを用意
$ cd #ホームディレクトリに移動 $ pwd #ホームディレクトリにいるかどうか確認 $ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" #コマンドを実行※処理に時間がかかる可能性のある操作です。
「
Press RETURN to continue or any other key to abort
」と表示されたら、「続けるにはエンターキーを、やめるにはそれ以外の入力をしてください
」という意味ですので、エンターキーを入力します。「
Password:
」と表示されたら、PCを開く際に求められるパスワードを入力します。ターミナル上で文字は表示されませんが、間違いなく入力はされています。パスワードを入力し終わったらエンターキーを押します。$ brew -v #Homebrewがインストールされているか確認$ brew -v Homebrew 2.1.13$ brew update #Homebrewをアップデート$ sudo chown -R `whoami`:admin /usr/local/bin #Homebrewの権限を変更❸新しいバージョンのRubyをインストール
$ brew install rbenv ruby-build #Homebrewを用いてrbenvとruby-buildをインストール$ echo 'eval "$(rbenv init -)"' >> ~/.zshrc #PCにおけるどこのディレクトリからも使用できるようにする$ source ~/.zshrc #zshrcの変更を反映$ brew install readline #readlineをインストール$ brew link readline --force #どこからも使用できるように# rbenvを利用してRubyをインストール $ RUBY_CONFIGURE_OPTS="--with-readline-dir=$(brew --prefix readline)" $ rbenv install 2.5.1※処理に時間がかかる可能性のある操作です。
$ rbenv global 2.5.1 #利用するRubyのバージョンを指定$ rbenv rehash #rbenvを読み込んで変更を反映$ ruby -v #Rubyのバージョンを確認❹MySQLを用意
# MySQLのインストール $ brew install mysql@5.6 $ brew reinstall https://raw.githubusercontent.com/Homebrew/homebrew-core/f171f1c74/Formula/mysql@5.6.rb※処理に時間がかかる可能性のある操作です。
# MySQLの自動起動設定 $ mkdir ~/Library/LaunchAgents $ ln -sfv /usr/local/opt/mysql\@5.6/*.plist ~/Library/LaunchAgents $ launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql\@5.6.plist# mysqlのコマンドをどこからでも実行できるようにする $ echo 'export PATH="/usr/local/opt/mysql@5.6/bin:$PATH"' >> ~/.zshrc $ source ~/.zshrc# mysqlのコマンドが打てるか確認する $ which mysql# 以下のように表示されれば成功 /usr/local/opt/mysql@5.6/bin/mysql❺Railsを用意
$ gem install bundler #bundlerをインストール$ gem install rails --version='5.2.3' #Railsをインストール$ rbenv rehash #rbenvを再読み込みしておく$ rails -v #Railsが導入できたか確認Catalina以降の場合は、これにてWebアプリケーション開発のための環境構築は完了です。
- 投稿日:2019-11-29T15:07:32+09:00
#Rails ( or #Ruby + ActiveSupport ) : Time.zone.now VS Time.current what is difference? when specify Time.zone then used ActiveSupport::TimeWithZone class else use Time class
require 'active_support/core_ext' # No Time.zone setting Time.use_zone(nil) { Time.current } # => 2019-11-29 07:38:04 +0900 Time.use_zone(nil) { Time.current.class } # => Time # with Time.zone setting # Time.current class changed Time to ActiveSupport::TimeWithZone Time.use_zone('UTC') { Time.current } # => Thu, 28 Nov 2019 22:38:20 UTC +00:00 Time.use_zone('UTC') { Time.current.class } # => ActiveSupport::TimeWithZone Time.use_zone('UTC') { Time.zone.now } # => Thu, 28 Nov 2019 22:38:26 UTC +00:00 Time.use_zone('UTC') { Time.zone.now.class } # => ActiveSupport::TimeWithZone require 'active_support/testing/time_helpers' include ActiveSupport::Testing::TimeHelpers # Freeze time Time.use_zone('UTC') { travel_to Time.parse('2020-01-01 00:00') } Time.use_zone('UTC') { Time.current == Time.zone.now } # => true # NOTE # Time.parse class not changed to ActiveSupport::TimeWithZone Time.parse('2020-01-01 00:00') # => 2020-01-01 00:00:00 +0900 Time.parse('2020-01-01 00:00').class # => Time Time.use_zone('UTC') { Time.zone.parse('2020-01-01 00:00') } # => Wed, 01 Jan 2020 00:00:00 UTC +00:00 Time.use_zone('UTC') { Time.zone.parse('2020-01-01 00:00').class } # => ActiveSupport::TimeWithZoneOriginal by Github issue
- 投稿日:2019-11-29T15:07:31+09:00
#Rails ( or #Ruby + ActiveSupport ) + travel_to : freeze to localtime beggining of year and ref UTC date and time and timestamp
# REQUIRE : on Ruby require 'active_support/testing/time_helpers' # => true require 'active_support/core_ext' # => true include ActiveSupport::Testing::TimeHelpers # => Object # Travel to JST beggining of Year travel_to Time.use_zone('Tokyo') { Time.parse('2020-01-01 00:00') } # => nil # JST Time.use_zone('Tokyo') { Time.current } # => Wed, 01 Jan 2020 00:00:00 JST +09:00 Time.use_zone('Tokyo') { Time.zone.now } # => Wed, 01 Jan 2020 00:00:00 JST +09:00 Time.use_zone('Tokyo') { Date.current } # => Wed, 01 Jan 2020 # UTC Time.use_zone('UTC') { Time.current } # => Tue, 31 Dec 2019 15:00:00 UTC +00:00 Time.use_zone('UTC') { Time.zone.now } # => Tue, 31 Dec 2019 15:00:00 UTC +00:00 Time.use_zone('UTC') { Date.current } # => Tue, 31 Dec 2019 # Unix Timestamp # are All same at any Timezone Time.use_zone('Tokyo') { Time.zone.now.to_i } # => 1577804400 Time.use_zone('UTC') { Time.zone.now.to_i } # => 1577804400 Time.now.to_i # => 1577804400 Time.current.to_i # => 1577804400 # UTC vs JST near date change line travel_to Time.use_zone('Tokyo') { Time.parse('2020-01-01 08:59') } Time.use_zone('UTC') { Date.current } # => Tue, 31 Dec 2019 travel_to Time.use_zone('Tokyo') { Time.parse('2020-01-01 09:00') } # => nil Time.use_zone('UTC') { Date.current } # => Wed, 01 Jan 2020Original by Github issue
- 投稿日:2019-11-29T15:07:31+09:00
#Rails ( or #Ruby + ActiveSupport ) の Time.zone.now と Time.current の違いは Time.zone 指定の有無であり、Time class と ActiveSupport::TimeWithZone class が切り替わることが判明
require 'active_support/core_ext' # No Time.zone setting Time.use_zone(nil) { Time.current } # => 2019-11-29 07:38:04 +0900 Time.use_zone(nil) { Time.current.class } # => Time # with Time.zone setting # Time.current class changed Time to ActiveSupport::TimeWithZone Time.use_zone('UTC') { Time.current } # => Thu, 28 Nov 2019 22:38:20 UTC +00:00 Time.use_zone('UTC') { Time.current.class } # => ActiveSupport::TimeWithZone Time.use_zone('UTC') { Time.zone.now } # => Thu, 28 Nov 2019 22:38:26 UTC +00:00 Time.use_zone('UTC') { Time.zone.now.class } # => ActiveSupport::TimeWithZone require 'active_support/testing/time_helpers' include ActiveSupport::Testing::TimeHelpers # Freeze time Time.use_zone('UTC') { travel_to Time.parse('2020-01-01 00:00') } Time.use_zone('UTC') { Time.current == Time.zone.now } # => true # NOTE # Time.parse class not changed to ActiveSupport::TimeWithZone Time.parse('2020-01-01 00:00') # => 2020-01-01 00:00:00 +0900 Time.parse('2020-01-01 00:00').class # => Time Time.use_zone('UTC') { Time.zone.parse('2020-01-01 00:00') } # => Wed, 01 Jan 2020 00:00:00 UTC +00:00 Time.use_zone('UTC') { Time.zone.parse('2020-01-01 00:00').class } # => ActiveSupport::TimeWithZoneOriginal by Github issue
- 投稿日:2019-11-29T15:06:56+09:00
初めてのRuby on Rails
はじめに
このQiitaは、プログラミング未経験の初心者がRailsチュートリアルに取り組み
学んだことを記録、振り返るために書いていきます。今までやったこと
Progateで
- HTML
- CSS
- Ruby
- Ruby on Rails5
- Git
- CommandLine
- SQL
- HTMLとCSSを学んだ後に一度架空のホームページの見た目だけ作成
PC環境
MacBook Pro Mid2012(古いです)
macOS Mojave ver 10.14.6
エディタ
- Visual Studio Code ver 1.40.2
最初のアプリケーション
「Hello,World」を表示するプログラム
1.ターミナルでcd..... ホームディレクトリに移動
2.cd environment.....作成したenvironmentディレクトリに移動
3.rails new.....railsアプリの骨組みを作成ターミナルで
$ rails _5.1.6_ new hello_appこれでrails 5.1.6を使ったhello_appというファイルが作られる
rails newとは
新規ファイルの作成
RsilsはRubyを使ったフレームワークなのである程度必要なファイルなどを自動作成してくれる。
$ rails new blog_app(作りたいプロジェクト名)これによりblog_appというファイルが作られ、必要なものが中に自動生成される。
rails newのオプション
rails newをする際に様々な条件を付けることができる
Railsのバージョンの指定
railsのバージョンを指定して作成できる
$ rails new _5.1.6_ new blog.app5.1.6の部分に好きなバージョンを入れることで、そのバージョンで作成できる。
データベースの指定
rails new時、データベースのデフォルトはsqliteになる。
rails new した後に変更することも可能だが、最初から使うデータベースが決まっているなら最初に設定しておける。$ rails new blog_app -database=mysqlこれはMySQLを使うときのコマンド
$rails new blog_app -database=postgresqlこれはPostgresqlを使うときのコマンド
そのほかにもrails new するときのオプションはたくさんある
また今度調べよう
Bundler
Bundlerは必要なgemをインストールするコマンド
$ budle※railsにおけるgemとはrubyのライブラリのこと
rubyに必要な機能の管理場所のようなもの通常rails newをすると最低限必要なgemは自動でbundlerしてくれる
チュートリアル通りgemの内容を変更し、bundlerを再実行する
gemはテキストエディタでhello_appディレクトリにあるgemfileで触れる
今回は下記の様にgemを書き換える
source 'https://rubygems.org' gem 'rails', '5.1.6' gem 'puma', '3.9.1' gem 'sass-rails', '5.0.6' gem 'uglifier', '3.2.0' gem 'coffee-rails', '4.2.2' gem 'jquery-rails', '4.3.1' gem 'turbolinks', '5.0.1' gem 'jbuilder', '2.6.4' group :development, :test do gem 'sqlite3', '1.3.13' gem 'byebug', '9.0.6', platform: :mri end group :development do gem 'web-console', '3.5.1' gem 'listen', '3.1.5' gem 'spring', '2.0.2' gem 'spring-watcher-listen', '2.0.1' end書き換えたらターミナルでbundle
発生したエラー
You have requested: spring = 2.0.2 The bundle currently has spring locked at 2.1.0. Try running `bundle update spring` If you are updating multiple gems in your Gemfile at once, try passing them all to `bundle update`あなたがリクエストしたのはspring = 2.0.2だけどspring = 2.1.0で設定してるから無理だよ!
変えるならbundle updateしてね!
的なことを言っているので素直にbundle updateを実行
エラーは治ったそして再度bundle
正常に作動して必要なgemをインストールした
Railsサーバーの立ち上げ
$ rails serverrailsはこのコマンドを入れるだけで、ローカルWebサーバーを立ち上げることができる
$ rails sでもいいらしい
rails s をするときは別ターミナルタブで行った方が他のコマンドも打てるので良い
入力したら
にアクセスしてみる
サーバーの立ち上げに成功していれば
Yay! You’r on Rails! と可愛らしいイラスト付きで表示が出る。
Railsのサーバーの立ち上げができた
立ち上げること自体は本当に簡単でたった数個の記述で出来た
数行の記述でブログが作れる!みたいな記事もあったので
このお手軽さがrailsが初学者に人気の理由なのかなRailsチュートリアル自体が最初は細かい部分が省かれている感じなので
rails全体を掴むために疑問点はメモしておき、
確認の1週目ということにして書いてある通りにさくさく進めてみよう
- 投稿日:2019-11-29T14:50:20+09:00
rubyのバージョンアップ
rubyのバージョンアップする時の方法
この記事は私の備忘録として記載していきます。
何か間違っている場合があれば、ご指摘していただけると助かります。現在の環境
ruby 2.6.3
rails 5.2.3目的
rubyのバージョンを2.6.5にしたいです。
環境にバージョンがあるか確認します。
$ rbenv install --listもし、指定したいバージョンがなければ
$ brew upgrade ruby-buildをしてください。
もう一度確認で
$ rbenv install --listrbenvでインストール
指定したいバージョンがあれば次に、rbenvでインストールをします。
$ rbenv install 2.6.5インストールされました!!
インストールしたバージョンを使用する時
$ rbenv versions system 2.4.1 2.5.1 2.5.3 * 2.6.3 (set by /Users/user_name/hoge/.ruby-version) 2.6.5
- 環境全体にrubyバージョンを反映したい時
$ rbenv global 2.6.5
- 特定のプロジェクトのみ反映したい時
$ rbenv local 2.6.5もう一度確認で
$ rbenv versions system 2.4.1 2.5.1 2.5.3 2.6.3 * 2.6.5(set by /Users/user_name/hoge/.ruby-version)反映されました。
rubyのバージョンごとにgemをインストールしなければいけないので
$ gem install bundlerインストールできました。
Gemfileに使用するバージョンを書き換えて$ bundle install --path=vendor/bundleできました。
rubyのバージョンを上げた後にrailsコマンドを使おうとすると、下記のようになってしまった。
ruby.rb$ rails --version rbenv: rails: command not found The `rails' command exists in these Ruby versions: 2.6.5新しくインストールしたrubyの中にrailsというgemが入っていないために起こっているようだ。
目的
新しくインストールしたruby環境にrailsをインストールをする
まず、gemをアップデートします$ gem update --system次にbundlerをインストールします
$ gem install bundlerその次にrailsをインストールします
$ gem install railsバージョンを確認します。
$ rails --version Rails 5.2.3バージョンが表示されたらrails コマンドが使えるようになっている
まとめ
初めてのQiita記事でした。記事一つ書くだけでも長時間かかってしまいました。
本日はバージョンのアップデートやバージョン指定など頻繁にあるので、備忘録としてここに書かせて頂きました。
どんどん記事を書いてアウトプットしていきたいと思います。この記事は、下記の記事を参考にさせて頂きました。
rbenvでrubyのバージョンを上げたときに The `rails' command exists in these Ruby versions: となる
とても、勉強になりました。ありがとうございます。
- 投稿日:2019-11-29T14:46:18+09:00
【Ruby on Rails】新規アプリケーション作成まとめ
Rubyとは
Ruby(プログラミンング言語)
Rubyはプログラミング言語の一つであり、Javaなどの他のプログラミング言語に比べて、少ないソースコードの記述量でプログラムを組めるなどの特徴があります。(Ruby公式サイト)
Ruby on Rails
Ruby on Rails
RubyのWebアプリケーションフレームワークの一つであり、Webアプリケーションフレームワークとは、Webアプリケーションを簡単に作れるようにする骨組みのことです。
フレームワークを使用することで、開発者はより少ない労力で開発を行うことが可能になります。Rails開発環境の構築
Rails開発ではデータベース(MySQL)やRails自体のインストールなど、まずは開発のできる環境を整える必要があります。(Rails開発環境構築)
- Homebrewのインストール・アップデート
- rbenv・ruby-buildのインストール
- Ruby ver.2.5.1のインストール
- MySQLのインストール
- MySQLのインストール
新規アプリケーションを作成
$ rails new アプリケーション名 # アプリケーションを新規作成 $ rails new アプリケーション名 -オプション名 # オプションを付けてアプリケーションを作成Gemの追加
Gem
RailsはGem(じぇむ)と呼ばれる便利なrubyライブラリをインストールして利用することで、より簡単にアプリケーション開発をすることができます。gemには開発を効率化してくれたり、会員登録サービスが簡単にできるなど便利なものがたくさんあります。Railsアプリケーションは全てをゼロから作るのではなくgemのライブラリも利用することでより効率的に開発することができます。
Gemfileに追加したら必ずターミナルでbundle installを実行します。
gem 'Gem名' # Gemの追加$ bundle install # Gemをインストールデータベースの作成
データベース
データベースとはたくさんの情報(データ)が入っている箱のようなものです。この中には様々な種類のデータが存在していて、データを格納したり、取り出すことができます。Railsでは運用環境ごとにひとつのデータベースを持っています。
運用環境
運用環境 概要 development 開発環境。通常、開発をする際に使用する環境。 production 本番環境。アプリケーションを実際にリリースする際に使用する環境。 test テスト環境。アプリケーションの動作をテストする際に使用する環境。 database.ymlファイルの内容に基づいてデータベースを新規作成します。
$ rake db:create # データベースの作成
- 投稿日:2019-11-29T14:35:59+09:00
#Rails ( or #Ruby + ActiveSupport ) + travel で 現地時刻 = JST で特定日付に時間を固定して、それぞれUTC JST timestamp で現在時刻・日付を得る例
# REQUIRE : on Ruby require 'active_support/testing/time_helpers' # => true require 'active_support/core_ext' # => true include ActiveSupport::Testing::TimeHelpers # => Object # Travel to JST beggining of Year travel_to Time.use_zone('Tokyo') { Time.parse('2020-01-01 00:00') } # => nil # JST Time.use_zone('Tokyo') { Time.now } # => 2020-01-01 00:00:00 +0900 Time.use_zone('Tokyo') { Time.current } # => Wed, 01 Jan 2020 00:00:00 JST +09:00 Time.use_zone('Tokyo') { Time.zone.now } # => Wed, 01 Jan 2020 00:00:00 JST +09:00 Time.use_zone('Tokyo') { Date.current } # => Wed, 01 Jan 2020 # UTC Time.use_zone('UTC') { Time.now } # => 2020-01-01 00:00:00 +0900 Time.use_zone('UTC') { Time.current } # => Tue, 31 Dec 2019 15:00:00 UTC +00:00 Time.use_zone('UTC') { Time.zone.now } # => Tue, 31 Dec 2019 15:00:00 UTC +00:00 Time.use_zone('UTC') { Date.current } # => Tue, 31 Dec 2019 # Unix Timestamp # are All same at any Timezone Time.use_zone('Tokyo') { Time.now.to_i } # => 1577804400 Time.use_zone('Tokyo') { Time.zone.now.to_i } # => 1577804400 Time.use_zone('UTC') { Time.now.to_i } # => 1577804400 Time.use_zone('UTC') { Time.zone.now.to_i } # => 1577804400 Time.now.to_i # => 1577804400 Time.current.to_i # => 1577804400 # Date.today # Not considered Timezone with Time.zone setting # its Ruby method refers TZ env Time.use_zone('Tokyo') { Date.today } # => Wed, 01 Jan 2020 Time.use_zone('UTC') { Date.today } # => Wed, 01 Jan 2020Original by Github issue
- 投稿日:2019-11-29T13:55:59+09:00
やさしい図解で学ぶ モデル マイグレーション その2 アソシエーション
その1ではテーブルの設計図となるマイグレーションファイルにて紹介しましたが今回は「モデルについて」と「アソシエーション (関連性)」をモデルファイルへ記述する方法をカンタンに学んでいきます。
⭐️モデルについて
まずモデルファイルとは
DBとのデータのやりとりをするために重要なファイルとイメージしてください。ファイルには
モデル(テーブル)間の関係性を記述したり、
DBにあるテーブルに直接データの保存、呼び出しを可能にする機能を基本的に備えています。上の図では
モデルファイルからDBへ「ActiveRecord」と書かれたパイプや橋のようなものがあり、userやtweetモデルからDB内の同じ名前のテーブルに伸びています。*ActiveRecordというのは厳密にいうとRailsに標準ライブラリとして採用されているO/Rマッパーのことです。専門用語を使わず簡単に表現すると
DBとアプリケーションとの 「 橋渡し or 通訳 」
のようなものだと思ってください。
もし、モデルがないと
Railsアプリケーションではプログラミング言語「Ruby」を。
DBでは「SQL」をそれぞれ使っているので、言語の違いからそのままではやりとりができません。とざっくりなイメージですが、ありがたいことにRailsではモデルファイル(ActiveRecord)が通訳のようにRuby→SQLに、SQL→Rubyにと言語を変換してデータベースとのやりとりを円滑に行ってくれているんです。
⭐️アソシエーション(テーブル間の関連性)
アソシエーションとは、つまり
テーブル同士の関連性のことで、テーブル間にみられる対1や対多などの関係性をモデルファイルに明示しなければいけません。中間テーブルを含む3つのテーブルがあります。
これらの関係性を各テーブルのモデルファイルに記述していきます。まず
今回のテーブル群のモデルファイルへの記述方法は以下の3パターンを使用します。
(そのほかにもhas_oneやhas_and_belongs_to_manyなどがあり、オプションも存在しますが今回はこちらの3つのみ)belongs_toやhas_oneの場合は単数、
has_manyなどは複数形で記述します。実際にuserのモデルファイルに記述するとこのようになります。
またそのほかのモデルファイルにもアソシエーションを記述し、
さらにER図のようにイメージして書くとこのような関係性になります。中間テーブルcourse_usersにはそれぞれ接続されているテーブルの主キー、つまりは、外部キーuser_idとcourse_idを持っていますのでこのような記述となります。
そして中間テーブルと接続しているテーブルのモデルにはそれぞれアソシエーションとして以下のように記述します。
今回はイメージを含めて基本的なもののみの紹介です。
そのほかにも便利なオプションや外部キー関連の記述もありますので徐々に学んでいきましょう。⭐️補足:belongs_to
はじめはbelongs_toというのがちょっとわかりづらいと思いますが、考え方としてはテーブル内に外部キーを持っていた場合、そのモデルファイルにはbelongs_toを書くと思ってください。
例えば、
usesテーブルとtweetsテーブルなるものがあり、関係性が図のようだとするとtweetsテーブルには外部キーであるuser_idを持っています。
モデルファイルには以下のように記入します。
- 投稿日:2019-11-29T13:55:59+09:00
やさしい図解で学ぶ モデル マイグレーション その2
その1ではテーブルの設計図となるマイグレーションファイルにて紹介しましたが今回は「モデルについて」と「アソシエーション (関連性)について」を基礎に学んでいきます。
⭐️モデルについて
まずモデルファイルとは
DBとのデータのやりとりをするために重要なファイルとイメージしてください。ファイルには
モデル(テーブル)間の関係性を記述したり、
DBにあるテーブルに直接データの保存、呼び出しを可能にする機能を備えています。上の図では
モデルからDBへ「ActiveRecord」と書かれたパイプや橋のようなものがあり、userやtweetモデルからDB内の同じ名前のテーブルに伸びています。*ActiveRecordというのはRailsに標準ライブラリとして採用されているO/Rマッパーのことです。
専門用語を使わず簡単に表現すると
DBとアプリケーションとの 「 橋渡し or 通訳 」
のようなものだと思ってください。
もし、モデルがないと
Railsではプログラミング言語「Ruby」を。
DBでは「SQL」をそれぞれ使っているので、言語の違いからそのままではやりとりができません。Railsではモデルファイル(ActiveRecord)が通訳のようにRuby→SQLに、SQL→Rubyにと言語を変換してデータベースとのやりとりを円滑に行ってくれているんです。
⭐️アソシエーション(テーブル間の関連性)
アソシエーションとは、つまり
テーブル同士の関連性のことで、テーブル間にみられる対1や対多などの関係性をモデルファイルに明示しなければいけません。中間テーブルを含む3つのテーブルがあります。
これらの関係性を各テーブルのモデルファイルに記述していきます。今回の記述方法は以下の3パターンを使用します。
(そのほかにもhas_oneやhas_and_belongs_to_manyなどがあり、オプションも存在しますが今回はこちらの3つのみ)belongs_toやhas_oneの場合は単数、
has_manyなどは複数形で記述します。実際にuserのモデルファイルに記述するとこのようになります。
またそのほかのモデルファイルにもアソシエーションを記述し、
さらにER図のようにイメージして書くとこのような関係性になります。中間テーブルcourse_usersにはそれぞれ接続されているテーブルの主キー、つまりは、外部キーuser_idとcourse_idを持っていますのでこのような記述となります。
has_many through
中間テーブルと接続しているテーブルのモデルにはそれぞれアソシエーションとして以下のように記述します。
中間テーブルを通ってその先のインスタンスを複数持っていますよという記述になります。
今回も基礎的な部分を図解を用いて記載致しましたので実際の開発ではモデルファイルにもう少し細かく書いていきます。
今回の記事でモデルに関して少しでもイメージが湧いていただけたのなら幸いです。
⭐️補足:belongs_to
初見ではbelongs_toというのがちょっとわかりづらいと思いますが、考え方としてはテーブル内に外部キーを持っていた場合、そのモデルファイルにはbelongs_toを書くと思ってください。
例えば、
usesテーブルとtweetsテーブルなるものがあり、関係性が図のようだとするとtweetsテーブルには外部キーであるuser_idを持っています。
モデルファイルには以下のように記入します。
- 投稿日:2019-11-29T13:00:34+09:00
応用カリキュラム 09
renderメソッドをコントローラーで使う
ビューで使う場合と、コントローラで使う場合で動きが違うっぽい。
ビューで使う場合は(主に?)部分テンプレートとして使う。<%# _tweet.html.erbファイルをテンプレートとして使う場合 %> <%= render partial: ‘tweet’ %>となるが、
コントローラの場合は、特定のアクションのビューを呼び出す。render action: :new <%# 同じコントローラ内のnewアクションのビューを呼び出す %> render template: "review/new" <%# 違うコントローラ内のnewアクションのビューを呼び出す %>redirect_toと比較して、HTTPリクエストを送らない、のが特徴らしい。
モデルインスタンス.users << current_user で特定のグループのユーザー集合にログインユーザーを追加
モデルインスタンス.save データ保存が成功したか否かを返すメソッド
「if (last_message = messages.last).present?」と記述することで、最新のメッセージを変数last_messageに代入しつつ、メッセージが保存されているかどうかで場合分けを行なう。
if文を省略して1行に書く書き方を三項演算子と呼ぶ
条件式 ? trueの時の処理 : falseの時の処理
railsにおけるDBの値の表示
まず、DBの値にアクセスするには、コントローラかモデルでインスタンス変数を定義しておく必要がある、と自分は思い込んでいた。
ユーザー管理が必要なサービスでdeviseを適用しているならば、コントローラでのインスタンス変数定義より先にdeviseに関係するインスタンス変数とかローカル変数が定義されており、そのdeviseの変数を介してテーブル(モデル定義時にdeviseオプションをつけたテーブル)にアクセスすることが可能。
更にテーブル(モデル)同士のアソシエーションを繋いで定義しているならば、コントローラでインスタンス変数を定義せずとも、deviseの変数を介してテーブルを参照することができる。current_user.group.messageみたいな表示ができる。current_user.groups
current_user : device経由でuserモデルを定義した時に、userテーブルと紐づいたインスタンスがcurrent_userをはじめその他の変数に定義されてる
.groups : usersテーブルからアソシエーション定義によって紐づいたgroupsテーブルの情報を取得することができる。覚えたいことは、ビューで変数を参照したい場合でも必ずしもコントローラでインスタンス変数の定義をする必要はない、ということ。
HTTPリクエストからのルーティング⇨コントローラ⇨ビューについて
アクションに対応するレスポンスとして、ビューを表示するのみ、であったなら
コントローラにアクションメソッドを定義する必要がない。(ちゃんとビューファイルを用意しておけば)ルーティング⇨(コントローラ飛ばして)ビュー、となる。indexアクションへオプション(通知用メッセージ)付きでリダイレクト
redirect_to ({:action => 'index'}), :notice => 'message'
- 投稿日:2019-11-29T11:33:56+09:00
カラムを追加・削除・編集する方法
カラムを追加・削除・編集したい時ってあるよね〜
テーブルを作ったはいいものの後々で必要なカラムが出てくることありますよね?
カラムの洗い出しが甘いって?そのとおり!ではいってみよう!!
カラムの追加
ターミナルでマイグレーションファイルを作成
rails g migration Addカラム名Toテーブル名 カラム名:型
ターミナル① rails g migration AddTitleToTweets title:string ② rails g migration AddDetialsToTweets title:string date:date #複数選択も可能 #Detailsの部分は何でもOKマイグレーションファイル① class AddTitleToTweets < ActiveRecord::Migration[5.2] def change add_column :tweets, :title, :string end end ② class AddTitleToTweets < ActiveRecord::Migration[5.2] def change add_column :tweets, :title, :string add_column :tweets, :date, :date end end
いざ実行!!ターミナルrails db:migrate
カラムの削除
ターミナルでマイグレーションファイルを作成
rails g migration Removeカラム名Fromテーブル名 カラム名:型
ターミナルrails g migration RemoveTitleFromTweets title:stringマイグレーションファイルclass RemoveTitleFromTweets < ActiveRecord::Migration[5.2] def change remove_column :tweets, :title, :string end end
いざ実行!!ターミナルrails db:migrate
カラム名変更
ターミナルでマイグレーションファイルを作成
rails g migration rename_変更前のカラム名_column_to_テーブル名
ターミナルrails g migration rename_title_column_to_tweets
changeメソッドの中に追記
rename_column :テーブル名, :変更前の名前, :変更後の名前
マイグレーションファイルclass RenameTitleColumnToTweets < ActiveRecord::Migration[5.2] def change rename_column :users, :title, :theme end end
いざ実行!!ターミナルrails db:migrate
データ型変更
ターミナルでマイグレーションファイルを作成
rails g migration ChangeDatatypeカラム名Ofテーブル名
ターミナルrails g migration ChangeDatatypeTitleOfTweetsマイグレーションファイルclass ChangeDatatypeTitleOfTweets < ActiveRecord::Migration[5.2] def change end end
changeメソッドの中に追記
change_column :テーブル名, :カラム名, :新しいデータ型
マイグレーションファイルclass ChangeDatatypeTitleOfTweets < ActiveRecord::Migration[5.2] def change change_column :tweets, :title, :text end end
いざ実行!!ターミナルrails db:migrate
ではまた!
- 投稿日:2019-11-29T11:27:16+09:00
カラムにインデックスを貼ろう
インデックスとは
カラムにインデックスを設定することで、
データ検索を高速化
させることができます!
使い方
マイグレーションファイルを作成
ターミナルrails g migration AddIndexToTweets
マイグレーションファイルに、
add_index :テーブル名, :カラム名
を追記マイグレーションファイルclass AddIndexToTweets < ActiveRecord::Migration[5.2] def change add_index :tweets, :text end end
複数ver.マイグレーションファイルclass AddIndexToTweets < ActiveRecord::Migration[5.2] def change add_index :tweets, [:text, :title] end end
デメリットも
①データを保存・更新する速度が遅くなる
②データベースの容量を使う
インデックスの使い所は、
【格納するデータが多い時】
【データの検索が頻繁に行われる時】
ではまた!
- 投稿日:2019-11-29T11:26:06+09:00
カラムに制約をかけよう
カラムに制約をかけ、予期せぬデータが保存されるのを防ごう
制約 意味 NOT NULL制約 空データはダメよ 一意性制約 他と被るのはダメよ 外部キー制約 外部キーに対応するテーブルのレコードがないとダメよ
NOT NULL制約
空データは禁止
カラムの後ろにnull: false
を追記マイグレーションファイルclass CreateTweets < ActiveRecord::Migration[5.2] def change create_table :tweets do |t| t.text :text, null: false t.timestamps end end endターミナルrails db:migrateこれでtextに値がないツイートは作れないようになりました。
一意性制約
他と同じデータは禁止
add_index :テーブル名, :カラム名, unique: true
を追記マイグレーションファイルclass CreateTweets < ActiveRecord::Migration[5.2] def change create_table :tweets do |t| t.text :text end add_index :tweets, :text, unique: true end endターミナルrails db:migrateこれで、同じ内容のテキストは保存できなくなりました。
バルス
とツイートできるのは1人だけ。早いもの勝ちとなります。
外部キー制約
外部キーに対応するテーブルのレコードがないとダメ
t.references :対応先のテーブル名, foreign_key: true
を追記マイグレーションファイルclass CreateTweets < ActiveRecord::Migration[5.2] def change create_table :tweets do |t| t.text :text t.references :user, foreign_key: true end end endターミナルrails db:migrateこれですべてのツイートが誰かしらのユーザーにひも付きます。
迷子のツイートが現れることはありません。また、
テーブル名_id
というカラムが自動で生成されます。
今回はuser_id
ですね。
ではまた!
- 投稿日:2019-11-29T09:27:31+09:00
Railsの基礎の基礎 DB ActiveRecord migration MVC
Railsを学ぶ際の基礎に当たるところをここに記載しておきます。
随時更新予定(2019年11月29日)DBを操作する言語:SQL
例:Mysql,Sqlite
CRUD
Create/Read/Update/DeleteActiveRecordとは
Databaseの処理をRubyから簡単に行うための仕組み
SQLをうまく扱うための仕組み ModelとDBのことをさす(厳密には違う)例:@contacts = Contanct.all
migrationとは
DBの変更をテキストで残すことができる
それによりgitで他の人と変更を共有することができるMVCとは
Model
View
Controller実際の流れ
1.controllerが最初にインターネットの通信を判断する役割。どのモデルに処理を依頼するか考える
2.Modelはデータベースからデータをとる、もしくは入れる。そして再びコントローラに戻る。
3.Modelのデータが取れたらそれを表示する。それを表示するViewをコントローラーが考える。
- 投稿日:2019-11-29T09:27:31+09:00
Railsの基礎の基礎 DB,ActiveRecord,migration ,MVC,HTTP,CRUD
Railsを学ぶ際の基礎に当たるところをここに記載しておきます。
随時更新予定(2019年11月29日)DBを操作する言語:SQL
例:Mysql,Sqlite
CRUD
Create/Read/Update/DeleteActiveRecordとは
Databaseの処理をRubyから簡単に行うための仕組み
SQLをうまく扱うための仕組み ModelとDBのことをさす(厳密には違う)例:@contacts = Contanct.all
migrationとは
DBの変更をテキストで残すことができる
それによりgitで他の人と変更を共有することができるMVCとは
Model
View
Controller実際の流れ
1.controllerが最初にインターネットの通信を判断する役割。どのモデルに処理を依頼するか考える
2.Modelはデータベースからデータをとる、もしくは入れる。そして再びコントローラに戻る。
3.Modelのデータが取れたらそれを表示する。それを表示するViewをコントローラーが考える。主なHTTPメソッドとCRUD
HTTPとは
Hypertext Transfer Protocolとは、WebブラウザがWebサーバと通信する際に主として使用する通信プロトコルであり、インターネット・プロトコル・スイートのメンバである。簡単にいうとホームページのファイルとかを受け渡しするときに使う約束
CRUDは?
DBとのやりとり
HTTPメソッド CRUD 意味 Get Select 取得する Post Insert 作成する Put Update 全更新する Patch Update 部分更新する Delete Delete 削除する
- 投稿日:2019-11-29T07:38:19+09:00
db/seeds.rbの中身を変更したらrails db:seedできない。そんなときにありがちな原因
何が起こったか
db/seeds.rb
の中身を変更したら、rails db:seed
が失敗するようになりました。# rails db:seed rails aborted! SyntaxError: /var/www/sample_app/db/seeds.rb:5: syntax error, unexpected tIDENTIFIER, expecting ')' admin: true) ^~~~~ /usr/local/bundle/gems/activesupport-5.1.6/lib/active_support/dependencies.rb:286:in `load' ...略この後やたら長いスタックトレースが流れてきて面食らいます。
原因
原因は、
db/seeds.rb
で、単に項目区切りのカンマを入れ忘れていたことでした。db/seeds.rb
を以下のように修正します。db/seeds.rbUser.create(name: "Example User", email: "example@railstutorial.org", password: "foobar", - password_confirmation: "foobar" + password_confirmation: "foobar", admin: true) ...略# rails db:seed今度こそ
rails db:seed
は成功するはずです。なお、rails db:seed
が正常に完了した場合は、シェルに何も表示されません。
- 投稿日:2019-11-29T06:08:55+09:00
Ruby on Rails と React Native で作る web & モバイルアプリ [モバイルアプリ編]
イントロダクション
目的
この記事は、Ruby on Rails と React Native で作る web & モバイルアプリ [webアプリ編] の続編です。Ruby on Rails で作った web アプリケーションのリソースを API 経由で React Native アプリケーションから操作します。今回は以下のような web とモバイルの両方に対応したアイパス認証付きのシンプルなタスク管理システムを作っています。
React Native の演習は行いたいけど Ruby on Rails 編には興味がない方は、Ruby on Rails アプリケーションを以下より clone してデプロイするか、今のところは https://zone-web.herokuapp.com/ を生かしているので、こちらを利用してください。
フレームワーク リポジトリ Ruby on Rails (web) https://github.com/ogihara-ryo/zone-web React Native (mobile) https://github.com/ogihara-ryo/zone-mobile 想定読者
私自身がまだモバイルアプリケーションを何もリリースしたことのない初学者であるため、初学者による初学者のための演習になっています。写経のみで進めていける演習になっていますが、多少のプログラミング全般の心得は必要になるかもしれません。私は最近 Ruby on Rails で作った web アプリを React Native アプリ対応しようと思っているのですが、この辺りの参考日本語記事が少なくて苦労しているため、同じような立場の人の助けになれれば嬉しく思います。ただ、筆者は前述の通り完全に素人なので、React Native のベストプラクティス等はまるで理解しておらず、とりあえずシンプルに動くものを作る、ぐらいのモチベーションでこの演習を作りました。そのため、技術的に正しくないコードや誤った説明が含まれている可能性があります。何かお気付きのことがありましたら編集リクエストを頂けると嬉しく思います。
もし、この記事を進めていく上で躓いた場合や理解できないことがあった場合は @OgiharaRyo までご連絡頂くか、現在300人ほど参加している技術質問 slack コミュ二ティを運営していますので、こちらで質問して頂ければと思います。(slack 招待リンク)
環境
2019年12月初旬執筆時点の最新版を使用します。
% node -v v13.3.0 % npm -v 6.13.2 % watchman -v 4.9.0 % expo-cli -V 3.10.2インストールに関しては先人の知恵と功績によって特に癖はないと思いますが、Mac で Homebrew が入っている環境であれば以下で大体いけます。いけなかったら何とかしてください。
% brew install node % npm install npm@latest -g % brew install watchman % npm install expo-cli --globalReact Native アプリケーションの開発
expo init
早速アプリケーションを作っていきましょう。まずは空っぽの React Native アプリケーションを
expo init
で作成します。Choose a template
と言われるのでblank
のまま return キーで進みます。% expo init zone-mobile ? Choose a template: (Use arrow keys) ----- Managed workflow ----- ❯ blank a minimal app as clean as an empty canvas blank (TypeScript) same as blank but with TypeScript configuration tabs several example screens and tabs using react-navigation ----- Bare workflow ----- minimal bare and minimal, just the essentials to get you started minimal (TypeScript) same as minimal but with TypeScript configuration次に
<The name of your app visible on the home screen>
とホームスクリーンに表示するアプリケーションの名前を尋ねられます。ここではZone
と入力しておきましょう。? Please enter a few initial configuration values. Read more: https://docs.expo.io/versions/latest/workflow/configuration/ › 50% completed { "expo": { "name": "<The name of your app visible on the home screen>", "slug": "zone-mobile" } }起動確認
早速このまま起動してみましょう。
% expo startブラウザーに localhost:19002 が立ち上がります。画面左下に注目してください。
開発中アプリケーションの動作確認方法は大きく分けると、シミュレーターを使う方法と実機を使う方法の2つがあります。Xcode と Command Line Tools をインストール済みのマシンであれば、
Run on iOS simulator
を選択するとシミュレーターが起動します。シミュレーターから Expo アプリケーションを起動してよしなに操作すれば下記のような画面が表示されます。実機を使う場合は、お手元のスマホでストアから Expo Client をインストールして、localhost:19002 に表示されているあなた専用の QR コードを読み込めば Expo Client アプリが起動して上記のような画面が表示されます。ちなみに私は通常 iPhone と Pixel の2台の実機を使いながら iOS と Android の両方を並行して動作確認しながら開発しています。
react-navigation
今回開発するアプリケーションは2つの画面を持ちます。1つはログインフォームを持つログイン画面、もう1つはタスク管理を行うメイン画面です。開発のはじめの第一歩としては、ログイン画面とメイン画面を用意して、お互いを行き来できるような実装を行います。前準備として、react-navigation をインストールします。
react-navigation
は画面間の遷移をハンドリングしてくれるライブラリです。ドキュメント に従って react-navigation 及びその依存ライブラリ react-native-gesture-handler, react-native-reanimated, react-native-screens をexpo install
します。% expo install react-navigation react-native-gesture-handler react-native-reanimated react-native-screens
それではナビゲーションを実装していきます。今回は、
App.js
にナビゲーションを置いて、src/screens/Login.js
とsrc/screens/Main.js
を互いに遷移できるようなコードを書いていきます。まずはログイン画面とメイン画面を用意します。ただお互いの画面に遷移するためのボタンが1つだけ置いてある簡素な画面です。Button
のonPress
イベントでthis.props.navigation.navigate('main')
のようなメソッドコールを行っているのがポイントです。src/screens/Login.jsimport React from 'react'; import { View, Button } from 'react-native'; export default class Login extends React.Component { render() { return ( <View> <Button title="ログイン" onPress={() => {this.props.navigation.navigate('main')}}> </Button> </View> ); } }src/screens/Main.jsimport React from 'react'; import { View, Button } from 'react-native'; export default class Main extends React.Component { render() { return ( <View> <Button title="ログアウト" onPress={() => {this.props.navigation.navigate('login')}}> </Button> </View> ); } }作った2つの画面をナビゲーターにセットします。今回は、
createSwitchNavigator
を使用します。App.jsimport React from 'react'; import { StyleSheet, View } from 'react-native'; import { createAppContainer, createSwitchNavigator } from 'react-navigation'; import Login from './src/screens/Login' import Main from './src/screens/Main' export default function App() { const MainNavigator = createAppContainer( createSwitchNavigator({ login: { screen: Login }, main: { screen: Main } }) ) return ( <View style={styles.container}> <MainNavigator /> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', alignItems: 'center', justifyContent: 'center', }, });これで2画面を遷移できるアプリケーションになりました。
ログイン
それでは、本格的にログインフォームを実装していきましょう。解説は後にして、一旦全てのコードを一気に実装してしまいましょう。記事内の web アプリケーションの URL は、ご自身のアプリケーションをデプロイされていたら差し替えてください。
src/screens/Login.jsimport React from 'react'; import { View, Text, Button, TextInput, ActivityIndicator, StyleSheet } from 'react-native'; export default class Login extends React.Component { constructor(props) { super(props); this.state = { accountId: '', password: '', loading: false, failed: false }; } onSubmit() { this.setState({ loading: true }) return ( fetch(`https://zone-web.herokuapp.com/api/login.json?account_id=${this.state.accountId}&password=${this.state.password}`) .then((response) => response.json()) .then((jsonData) => { this.setState({ loading: false }) if (jsonData['api_token']) { this.props.navigation.navigate('main') } else { this.setState({ failed: true }) } }) .catch((error) => console.error(error)) ) } loginButton() { if (this.state.loading) { return <ActivityIndicator size="small" /> } else { return <Button title="ログイン" onPress={() => {this.onSubmit()}} /> } } render() { return ( <View> {this.state.failed && <Text>ログインに失敗しました。</Text>} <TextInput style={styles.textInput} placeholder="アカウントID" onChangeText={(accountId) => this.setState({accountId})} /> <TextInput secureTextEntry={true} style={styles.textInput} placeholder="パスワード" onChangeText={(password) => this.setState({password})} /> {this.loginButton()} </View> ); } } const styles = StyleSheet.create({ textInput: { height: 60, width: 300, paddingLeft: 20, margin: 10, borderWidth: 1, borderRadius: 8, } });まずは動かしてみましょう。誤ったアカウントIDやパスワードでログインを試みたらエラーメッセージが表示され、正しいアカウントIDとパスワードでログインを試みたらメイン画面へ遷移することを確認してください。
さて、今回実装したのは比較的長いコードではありますが、この
Login
コンポーネントが何をしているかは、各state
がどのタイミングで更新されて、何に使われているのかを知ることで全容を把握することができます。コードと読み比べてみましょう。
state 更新タイミング 用途 accountId アカウントIDの TextInput
にユーザーが入力する度にonChangeText
イベントで現在の値がセットされます。web アプリケーションのログイン API に与えるパラメーターとしてセットします。 password パスワードの TextInput
にユーザーが入力する度にonChangeText
イベントで現在の値がセットされます。web アプリケーションのログイン API に与えるパラメーターとしてセットします。 loading web アプリケーションに fetch
でリクエストを送る前にtrue
にし、レスポンスを受け取った後にfalse
にします。ログインボタンが押された後、web アプリケーションと通信している間にログインボタンを ActivityIndicator
(いわゆるロード中のクルクル)に差し替えることで、ユーザーに通信中であることを伝えると同時に、ログインボタンを不要に連打されないようにします。failed ログイン API を叩いて API トークンが返ってこなかった場合はアカウントIDかパスワードが間違っていたとして true
にします。true の場合にログインに失敗した旨をユーザーに通知します。 APIトークンの保存とログインの永続化
ここまでのコードでは、APIトークンが返ってきた場合はログインに成功したとしてナビゲーターをメイン画面に変更していますが、このAPIトークンは後のタスク操作 API をコールするために使用するのでちゃんと保存しておく必要があります。また、API トークンを一度受け取ってしまえば次回以降のアプリケーション起動時はログイン画面を挟まずにメイン画面を表示した方が親切です。今回は
AsyncStorage
というキーバリューストレージシステムに API トークンを保存します。src/screens/Login.jsimport React from 'react'; - import { View, Text, Button, TextInput, ActivityIndicator, StyleSheet } from 'react-native'; + import { View, Text, Button, TextInput, ActivityIndicator, StyleSheet, AsyncStorage } from 'react-native'; export default class Login extends React.Component { constructor(props) { super(props); this.state = { accountId: '', password: '', loading: false, failed: false }; } + + async componentDidMount() { + if (await AsyncStorage.getItem('api_token')) { + this.props.navigation.navigate('main') + } + } onSubmit() { this.setState({ loading: true }) return ( fetch(`https://zone-web.herokuapp.com/api/login.json?account_id=${this.state.accountId}&password=${this.state.password}`) .then((response) => response.json()) .then((jsonData) => { this.setState({ loading: false }) if (jsonData['api_token']) { + AsyncStorage.setItem('api_token', jsonData['api_token']); this.props.navigation.navigate('main') } else { this.setState({ failed: true }) } }) .catch((error) => console.error(error)) ) }
componentDidMount
は、コンポーネントがツリーに挿入された直後に呼び出されます。この時、AsyncStorage
に既に API トークンが入っていた場合はナビゲーターをメイン画面に切り替えています。ログアウト
最後に、ログアウトの実装も行いましょう。これはシンプルで、
AsyncStorage
から API トークンを削除してナビゲーターをログイン画面へ切り替えるだけです。src/screens/Main.jsimport React from 'react'; - import { View, Button } from 'react-native'; + import { View, Button, AsyncStorage } from 'react-native'; export default class Main extends React.Component { + logout() { + AsyncStorage.removeItem('api_token'); + this.props.navigation.navigate('login') + } render() { return ( <View> <Button title="ログアウト" - onPress={() => {this.props.navigation.navigate('login')}}> + onPress={() => {this.logout()}}> </Button> </View> ); } }タスクの CRUD
各処理の雑な解説は後回しにして、まずは完成形です。アクションも DOM のレンダリングも全て同じファイルにまとめているので100行ほどあります。
src/screens/Main.jsimport React from 'react'; import { View, FlatList, TextInput, Button, AsyncStorage, ActivityIndicator, StyleSheet } from 'react-native'; import { CheckBox } from 'react-native-elements'; export default class Main extends React.Component { constructor(props) { super(props); this.state = { taskName: '', tasks: [], loading: '', apiToken: '' }; } async componentDidMount() { this.setState({ loading: true, apiToken: await AsyncStorage.getItem('api_token') }) fetch(`https://zone-web.herokuapp.com/api/tasks.json?api_token=${this.state.apiToken}`) .then((response) => response.json()) .then((jsonData) => (this.setState({ loading: false, tasks: jsonData }))) .catch((error) => console.error(error)); } submitCreateTask() { if (!this.state.taskName) return this.setState({ loading: true }) fetch(`https://zone-web.herokuapp.com/api/tasks`, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ api_token: this.state.apiToken, task: { name: this.state.taskName } }) }) .then(response => response.json()) .then(json => { this.setState({ tasks: this.state.tasks.concat(json), taskName: '', loading: false }) }) .catch((error) => console.error(error)); } changeFinished(item) { item.finished = !item.finished this.setState({ loading: true }) fetch(`https://zone-web.herokuapp.com/api/tasks/${item.id}`, { method: 'PATCH', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ api_token: this.state.apiToken, task: { finished: item.finished } }) }) .then(response => this.setState({ loading: false })) .catch((error) => console.error(error)); } createTaskButton() { if (this.state.loading) return <ActivityIndicator size="small" /> else return <Button title="作成" onPress={() => {this.submitCreateTask()}} /> } renderTasks() { if (this.state.loading) return <FlatList /> else { return( <FlatList data={this.state.tasks} keyExtractor={(item) => item.id.toString()} renderItem={({ item }) => ( <CheckBox title={item.name} checked={item.finished} onPress={() => this.changeFinished(item)} /> )} /> ) } } logout() { AsyncStorage.removeItem('api_token'); this.props.navigation.navigate('login') } render() { return ( <View> <View style={styles.form}> <TextInput style={styles.textInput} placeholder="タスク名" value={this.state.taskName} onChangeText={(taskName) => this.setState({taskName})} /> {this.createTaskButton()} </View> {this.renderTasks()} <View style={styles.logout}> <Button title="ログアウト" onPress={() => {this.logout()}} /> </View> </View> ); } } const styles = StyleSheet.create({ form: { margin: 40 }, textInput: { height: 60, width: 300, paddingLeft: 20, margin: 10, borderWidth: 1, borderRadius: 8, }, logout: { marginBottom: 20 } });
react-native-elements
のCheckBox
を利用するのでインストールします。% expo install react-native-elements
state
まずはログイン画面と同様に
Main
コンポーネントの持つ各state
の動きを把握しましょう。
state 更新タイミング 用途 taskName タスク作成フォームの TextInput
にユーザーが入力する度にonChangeText
イベントで現在の値がセットされます。また、タスク作成ボタンを押した時にTextInput
から現在の値を空にするために空文字列をセットします。web アプリケーションのタスク作成 API に与えるパラメーターとしてセットします。 tasks componentDidMount
で web アプリケーションのタスク一覧 API からタスク情報を取得してセットします。また、各タスクの完了状態のチェックボックスを操作した時に、該当のタスクのfinished
の値をチェックボックスの状態に応じて変更します。タスク一覧の表示に使います。 loading ログイン画面と同様、web アプリケーションに fetch
でリクエストを送る前にtrue
にし、レスポンスを受け取った後にfalse
にします。ログイン画面と同様、タスク一覧の取得、タスクの作成、タスクの完了状態の更新の API をコールした後、web アプリケーションと通信している間に各種 UI を ActivityIndicator
に差し替えることで、ユーザーに通信中であることを伝えると同時に、不要な操作をされないようにします。この挙動は UX を損ねるので気に入らなければ外してしまってユーザーに通信していることを意識させないようにしても良いでしょう。今回は web アプリケーションとやり取りしていることを明示するために操作の度にクルクルが表示されます。apiToken componentDidMount
でAsyncStorage
から API トークンを取り出してセットします。毎回 await AsyncStorage.getItem('api_token')
してストレージアクセスするのは非効率なので、一度 API トークンを取得したらstate
にキャッシュしています。表示
まずはタスク一覧の表示についてです。実際の表示部分は下記です。
renderTasks() { if (this.state.loading) return <FlatList /> else { return( <FlatList data={this.state.tasks} keyExtractor={(item) => item.id.toString()} renderItem={({ item }) => ( <CheckBox title={item.name} checked={item.finished} onPress={() => this.changeFinished(item)} /> )} /> ) } }ロード中(web アプリケーションとの通信中)は空っぽの
FlatList
を表示し、ロード中でない場合はtasks
に従ってアイテムを表示しています。React Native では、コンポーネントのstate
が更新された時にそのコンポーネントのrender
メソッドが実行されます。つまり、loading
のstate
が更新される度にこのコンポーネントのrender()
が走るため、if
に入ってきたりelse
に入ってきたりするわけですね。このFlatList
にはthis.state.tasks
を与えてCheckBox
をレンダリングしていますが、this.state.tasks
に値が入るのは以下の処理です。async componentDidMount() { this.setState({ loading: true, apiToken: await AsyncStorage.getItem('api_token') }) fetch(`https://zone-web.herokuapp.com/api/tasks.json?api_token=${this.state.apiToken}`) .then((response) => response.json()) .then((jsonData) => (this.setState({ loading: false, tasks: jsonData }))) .catch((error) => console.error(error)); }タスクの一覧は
componentDidMount
で行っています。componentDidMount
は、render
メソッドによる DOM 挿入直後に呼び出されます。このコンポーネントではcomponentDidMount
で web アプリケーションからタスク一覧を取得しているのでタスク一覧表じまでの順序としては以下のようになります。
- 初回の
render
で 空のFlatList
を表示componentDidMount
で API からタスクの一覧を取得- 取得後に
loading
とtasks
のstate
が変更されたことで再度render
が実行されてタスク一覧を表示作成
続いてタスクの作成機能について見ていきましょう。
render
メソッド内のタスク作成フォーム部分は以下です。<View style={styles.form}> <TextInput style={styles.textInput} placeholder="タスク名" value={this.state.taskName} onChangeText={(taskName) => this.setState({taskName})} /> {this.createTaskButton()} </View>今までと同様、ロード中は
ActivityIndicator
のクルクルを、ロード中でない場合は作成ボタンをレンダリングします。createTaskButton() { if (this.state.loading) return <ActivityIndicator size="small" /> else return <Button title="作成" onPress={() => {this.submitCreateTask()}} /> }作成ボタンの
onPress
イベントで呼ばれるのが以下です。タスク名の入力フィールドに何も入力されていなかった場合は何もせずに終了し、何か入力されていた場合は web アプリケーションのタスク作成 API をコールしています。レスポンスを受けたら、tasks
のstate
に今追加したタスクの情報を詰めて、taskName
を空にすることでタスク入力フォームのタスク名入力フィールド(TextInput
)を空にしています。submitCreateTask() { if (!this.state.taskName) return this.setState({ loading: true }) fetch(`https://zone-web.herokuapp.com/api/tasks`, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ api_token: this.state.apiToken, task: { name: this.state.taskName } }) }) .then(response => response.json()) .then(json => { this.setState({ tasks: this.state.tasks.concat(json), taskName: '', loading: false }) }) .catch((error) => console.error(error)); }更新
最後はチェックボックスの状態を変更した時の完了状態更新処理です。チェックボックスのレンダリング部分は、タスク一覧を表示した時の
FlatList
の各アイテムの中にあります。renderTasks() { if (this.state.loading) return <FlatList /> else { return( <FlatList data={this.state.tasks} keyExtractor={(item) => item.id.toString()} renderItem={({ item }) => ( <CheckBox title={item.name} checked={item.finished} onPress={() => this.changeFinished(item)} /> )} /> ) } }
CheckBox
のonPress
イベントで呼ばれるのが以下です。引数のitem
は参照渡しなので、item.finished
の論理を反転するとtasks
のstate
が更新されます。changeFinished(item) { item.finished = !item.finished this.setState({ loading: true }) fetch(`https://zone-web.herokuapp.com/api/tasks/${item.id}`, { method: 'PATCH', headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({ api_token: this.state.apiToken, task: { finished: item.finished } }) }) .then(response => this.setState({ loading: false })) .catch((error) => console.error(error)); }以上で React Native の演習は終了です。
終わりに
今回は2記事構成にて Ruby on Rails アプリケーションと React Native アプリケーションの連携を行いました。冒頭にも書きましたが、技術的に正しくないコードや誤った説明があったかもしれないので、何かお気付きのことがありましたら編集リクエストを頂けると嬉しく思います。React Native アプリケーション開発の日本語入門記事は Firebase を使ってのものが多く、既存の web アプリケーションに API を生やしつつモバイル対応するようなシチュエーションを想定した演習は少なく思えたので、本記事がどなたかの役に立てば嬉しく思います。
Ruby on Rails Advent Calendar 17日目は canecco さんの「ローカル通知に画像を表示する話」です。
- 投稿日:2019-11-29T05:24:34+09:00
【Rails】基本的なログイン機構【Rails Tutorial 8章まとめ】
ログイン機能
HTTPリクエスト(POST、GETなど)は送信後、リクエストを送信したという記録が残らないため、情報を保存することができない。
ログイン機能を実装するために、sessionメソッドと専用のコントローラを用意する。Sessionsコントローラ
Sessionsコントローラを作成する。
$ rails generate controller Sessions newnewアクションとビューはログイン画面で使用する。
SessionsコントローラにはUsersコントローラと同様、RESTfulなルーティングを設定する。
resourcesメソッドを使ってもよいが、editやshowなどのアクションは不要なので、必要なルーティングだけを手動で設定する。config/routes.rbRails.application.routes.draw do . . . get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users endルーティングは次のような状態になっている。
Sessionsコントローラ内に、空のcreateアクションとdestroyアクションを作っておく。また、Sessionsコントローラ用のテストを名前付きルートで書き直しておく。
test/controllers/sessions_controller_test.rbclass SessionsControllerTest < ActionDispatch::IntegrationTest test "should get new" do get login_path assert_response :success end endログインフォーム
新規登録フォームと同様に、ログインフォームを作成する。
フォームフィールドはメールアドレスとパスワード用だけでよい。app/views/sessions/new.html<% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(:session, url: login_path) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div>form_forメソッドの第一引数は:sessionとなっている(次項)。
ログイン失敗時の処理
ログイン失敗時の処理を書いていく。
ログインフォームから送信された情報は、新規登録の時と同様に、params変数にハッシュとして入っている。params = { session: { email: "foo@bar.com", password: "foobar" } }よって、キーを使うことでデータにアクセスできる。
params[:session][:email] params[:session][:password]これを使ってcreateアクションを作ると次のようになる。
app/controllers/sessions_controller.rbdef create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) # ユーザーログイン後にユーザー情報のページにリダイレクトする else # エラーメッセージを作成する render 'new' end endまず、find_byメソッドを使ってフォームに入力されたメールアドレスと一致するユーザーを探す。
ここで取得したUserオブジェクトは他のところでは使わないので、@付きのインスタンス変数にする必要はない。
また、メールアドレスは全て小文字で保存されているので、downcaseメソッドを使うことを忘れないようにする。次に、ユーザーが見つかれば、has_secure_passwordのauthenticateメソッドを使って、送信されたパスワードが正しいかどうかチェックする。
ユーザーが存在し、かつパスワードが合っていれば、ログインしてユーザー情報ページにリダイレクトする。
ユーザーが存在しない、またはパスワードが間違っていれば、エラーメッセージを表示して、ログインページに戻る。エラーメッセージ
ユーザーの新規登録時には、エラーメッセージは@user.errors.full_messagesに配列として入っており、それをeachメソッドを使って取り出して表示していた。
ログインの場合にはUserオブジェクトのようなものが無いので、flashメソッドを使って表示する。app/controllers/sessions_controller.rbdef create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) # ユーザーログイン後にユーザー情報のページにリダイレクトする else flash[:danger] = 'Invalid email/password combination' # 本当は正しくない render 'new' end endこのフラッシュによるエラーメッセージにはバグがある。
フラッシュはページを移動したり再読み込みすると消えるのだが、この場合は直後に再レンダリングしていることが原因で、メッセージが表示されたままになってしまう。
これをテスト駆動開発で解決していく。ログイン失敗時のテスト
ログイン機能用のテストを作成する。
$ rails generate integration_test sessions_users_loginSessionsコントローラ用のテストなのに、users_loginという名前は紛らわしいと思ったので、チュートリアルから少し変えている。
フラッシュがちゃんと消えているかも含め、ログイン失敗時のテストを書く。
test/integration/users_login_test.rbrequire 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest test "login with invalid information" do get login_path assert_template 'sessions/new' post login_path, params: { session: { email: "", password: "" } } assert_template 'sessions/new' assert_not flash.empty? get root_path assert flash.empty? end end手順は次のよう。
①ログインページに移動する。
②ログインページが表示されていることを確認する。
③無効なユーザー情報を持つparamsハッシュを送信する。
④ログインに失敗したために、ログインページが再レンダリングされたことを確認する。
⑤フラッシュによるエラーメッセージの存在を確認する。
⑥適当にページを移動する(今回はルートURL)。
⑦フラッシュによるエラーメッセージが消えていることを確認する。最後の⑦のところで実際はフラッシュが消えずに残っているので、テストはREDになる。
これを解決するために、flashをflash.nowに書き換える。
app/controllers/sessions_controller.rbflash.now[:danger] = 'Invalid email/password combination'flash.nowを使うと、再レンダリングしたページでのみフラッシュが表示されるようになる。
テストがGREENとなることを確認しておく。
ログイン成功時の処理
sessionsヘルパーメソッド
ログイン機能に必要なメソッドは、Sessionsコントローラを作成した際に自動で生成される。
これをApplicationコントローラで読み込んで、どのコントローラでも使えるようにしておく。app/controllers/application_controller.rbclass ApplicationController < ActionController::Base protect_from_forgery with: :exception include SessionsHelper endlog_inメソッド
sessionメソッドの中にハッシュとしてUserオブジェクトのidを入れることで、ログイン機能を実装できる。
session[:user_id] = user.idこれは様々なところで使うので、Sessionsコントローラのヘルパーメソッドとして作成する。
app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end endこれを使ってcreateアクションを書き換えると、次のようになる。
app/controllers/sessions_controller.rbdef create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) log_in user redirect_to user else flash.now[:danger] = 'Invalid email/password combination' render 'new' end endなお、redirect_to userはredirect_to user_url(user)としてもよい。
現在ログインしているユーザー
現在ログインしているユーザー、つまりsession[:user_id]に入っているidを持つユーザーを取得するために、current_userを定義する。
app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end # 現在ログイン中のユーザーを返す (いる場合) def current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end endログインしている場合、session[:user_id]とidが一致するユーザーをfind_byメソッドで探して返す。
ここでは||=(or equals)という代入演算子が用いられている。
以下の二文は同じであり@foo = @foo || "bar" @foo ||= "bar"これは「変数の値がnilなら変数に代入するが、nilでなければ代入しない (変数の値を変えない)」という操作である。
>> @foo => nil >> @foo = @foo || "bar" => "bar" >> @foo = @foo || "baz" => "bar"変数@fooに何も入っていない場合、右辺の値が代入される。
何か入っている場合、何も代入されない。このcurrent_userが呼び出されるたびに、ユーザーを探して@current_userに代入する、という無駄な処理を省く。
ログイン時のレイアウト
ログインしている場合と、ログインしていない場合とで、レイアウトに表示するものを変更する。
例えば、すでログインしている場合は新規登録やログインページへのリンクは不要である。
また、ログインユーザーのみがアクセスできるページへのリンクを表示する。これは埋め込みRubyとif文を使って次のように書ける。
<% if logged_in? %> # ログインユーザー用のリンク <% else %> # ログインしていないユーザー用のリンク <% end %>logged_in?メソッド
ここで、ログインしているならばtrueを返し、そうでないならばfalseを返すlogged_in?ヘルパーメソッドを作成する。
app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end # 現在ログイン中のユーザーを返す (いる場合) def current_user if session[:user_id] @current_user ||= User.find_by(id: session[:user_id]) end end # ユーザーがログインしていればtrue、その他ならfalseを返す def logged_in? !current_user.nil? end end変更したヘッダーのレイアウトは以下のよう。
app/views/layouts/_header.html<header class="navbar navbar-fixed-top navbar-inverse"> <div class="container"> <%= link_to "sample app", root_path, id: "logo" %> <nav> <ul class="nav navbar-nav navbar-right"> <li><%= link_to "Home", root_path %></li> <li><%= link_to "Help", help_path %></li> <% if logged_in? %> <li><%= link_to "Users", '#' %></li> <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> Account <b class="caret"></b> </a> <ul class="dropdown-menu"> <li><%= link_to "Profile", current_user %></li> <li><%= link_to "Settings", '#' %></li> <li class="divider"></li> <li> <%= link_to "Log out", logout_path, method: :delete %> </li> </ul> </li> <% else %> <li><%= link_to "Log in", login_path %></li> <% end %> </ul> </nav> </div> </header>ユーザーのプロフィールページへのリンクは以下のようであり、link_toメソッドの第二引数のcurrent_userは、やはりuser_path(current_user)とも書ける。
<li><%= link_to "Profile", current_user %></li>ログアウト用のリンクは以下のようであり、第三引数にmethod: :deleteを渡すことで、DELETEリクエストを送信できるようにしている。
<%= link_to "Log out", logout_path, method: :delete %>ログイン成功時のテスト
digestメソッドとテスト用ユーザー
ログイン機能をテストするために、有効なユーザー情報を持つテスト用のユーザーをfixtureファイルに用意する。
実際のアプリケーションでは、パスワードはbcryptによってハッシュ化されてUserモデルのpassword_digestカラムに保存される。
テストでこれを再現するために、文字列をbcryptと同じ方法でハッシュ化するdigestメソッドを作成する。app/models/user.rbclass User < ApplicationRecord . . . validates :password, presence: true, length: { minimum: 6 } # 渡された文字列のハッシュ値を返す def User.digest(string) cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost BCrypt::Password.create(string, cost: cost) end end内容の理解は不要。
digestメソッドは個々のUserオブジェクトに対して使うものではないので、インスタンスメソッドではなくクラスメソッドで定義している。テスト用のfixtureファイルは以下のようになる。
test/fixtures/users.ymlmichael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %>fixtureファイルは全ての行を一段インデントしておかないと謎のエラーを吐くので注意。
また、埋め込みRubyが使用できる。テスト
有効なユーザー情報でログインし、成功するテストを書く。
test/integration/users_login_test.rbrequire 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) end . . . test "login with valid information" do get login_path assert_template 'sessions/new' post login_path, params: { session: { email: @user.email, password: "password" } } assert_redirected_to @user follow_redirect! assert_template 'users/show' assert_select "a[href=?]", login_path, count: 0 assert_select "a[href=?]", logout_path assert_select "a[href=?]", user_path(@user) end endsetupでfixtureファイルのユーザーを@userに入れておく。
fixtureファイルのユーザーはusers(ユーザー名のキー)で取得できる。
このusersはusers.ymlのusersであり、Userモデルは関係ない。
また、users[:michael]と書くのは間違いなので注意する。テストの手順は以下のよう。
①ログインページに移動し、表示されているか確認する。
②有効なユーザー情報を持つparamsハッシュを送信する。
③ログインが成功し、ユーザーのプロフィールページにリダイレクトすることを確認する(assert_redirected_toを使う)。
④リダイレクト先に移動する。
⑤ユーザーのプロフィールページが表示されていることを確認する。
⑥ログインページへのリンクが消えていることを確認する。
⑦ログアウトページとプロフィールページへのリンクが表示されていることを確認する。ユーザー登録時にログインする
ユーザーが新規登録をした場合に自動でログインするようにする。
Usersコントローラのcreateアクションで、新規登録に成功した場合の処理にlog_inメソッドを追加する。app/controllers/users_controller.rbdef create @user = User.new(user_params) if @user.save log_in @user flash[:success] = "Welcome to the Sample App!" redirect_to @user else render 'new' end endユーザー登録時のログインのテスト
is_logged_in?メソッド
ユーザーの新規登録後に自動でログインできているかのテストを書く。
ここで、ログインしているかどうかを論理値で返すis_logged_in?ヘルパーメソッドを作成する。test/test_helper.rbENV['RAILS_ENV'] ||= 'test' . . . class ActiveSupport::TestCase # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. fixtures :all # Add more helper methods to be used by all tests here... include ApplicationHelper # テストユーザーがログイン中の場合にtrueを返す def is_logged_in? !session[:user_id].nil? end endこのis_logged_in?ヘルパーはSessionsコントローラのlogged_in?ヘルパーメソッドと動作は同じであるが、名前が違う。
アプリケーション用のヘルパーとテスト用のヘルパーは同じ機能でも名前を変えておかないとトラブルの原因になることがあるらしい。
ところで、logged_in?メソッドではcurrent_userを使ってログインを確かめていたが、こちらではcurrent_userが使えないので、sessionを使っている。
(むこうでsessionを使ってもよいと思う)テスト自体は、ユーザー登録用のテストに一行追加するだけで済む。
test/integration/users_signup_test.rbrequire 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest . . . test "valid signup information" do get signup_path assert_difference 'User.count', 1 do post users_path, params: { user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } } end follow_redirect! assert_template 'users/show' assert is_logged_in? assert_not flash.empty? end endログアウト機能
log_outメソッド
ログアウトするためのlog_outヘルパーメソッドを定義する。
app/helpers/sessions_helper.rbmodule SessionsHelper # 渡されたユーザーでログインする def log_in(user) session[:user_id] = user.id end . . . # 現在のユーザーをログアウトする def log_out session.delete(:user_id) @current_user = nil end endsessionの値を削除するためには、session.delete(:user_id)とする。
session[:user_id] = nilでもよい(多分)。
current_userも忘れずにnilにしておく。Sessionsコントローラのdestroyアクションでlog_outメソッドを使用する。
app/controllers/sessions_controller.rbdef destroy log_out redirect_to root_url endログアウト後はルートURLにリダイレクトする。
ログアウト機能のテスト
ログイン成功時のテストに加筆する形で、ログアウト機能のテストを書く。
test/integration/users_login_test.rbrequire 'test_helper' class UsersLoginTest < ActionDispatch::IntegrationTest . . . test "login with valid information followed by logout" do get login_path post login_path, params: { session: { email: @user.email, password: 'password' } } assert is_logged_in? assert_redirected_to @user follow_redirect! assert_template 'users/show' assert_select "a[href=?]", login_path, count: 0 assert_select "a[href=?]", logout_path assert_select "a[href=?]", user_path(@user) # ログアウト delete logout_path assert_not is_logged_in? assert_redirected_to root_url follow_redirect! assert_select "a[href=?]", login_path assert_select "a[href=?]", logout_path, count: 0 assert_select "a[href=?]", user_path(@user), count: 0 endログアウトのassert_selectは、ログインの逆になっている。
- 投稿日:2019-11-29T04:20:20+09:00
Ruby on Rails と React Native で作る web & モバイルアプリ [webアプリ編]
イントロダクション
目的
なんか web アプリケーションを作って運用していたら、どうやらモバイルアプリ需要が出てきたことが分かってバタバタ後追いでモバイルアプリ対応することってあるじゃないですか。今回は演習式でそのシミュレーションをします。まず、Ruby on Rails で web アプリケーションを作った後に、API を生やして React Native で web アプリケーションと同じような仕様のモバイルアプリケーションを作ります。
今回作るアプリケーションの完成品は以下のリポジトリに置いていますので、React Native 側の演習のみを行いたい方は Ruby on Rails アプリケーションは以下より clone してデプロイし、モバイルアプリ編 へ進んでください。
フレームワーク リポジトリ Ruby on Rails (web) https://github.com/ogihara-ryo/zone-web React Native (mobile) https://github.com/ogihara-ryo/zone-mobile いきなり言い訳なんですが、Advent Calendar の1枠としてやるには明らかにやりすぎな記事になってしまいました。お詫び申し上げます。Ruby on Rails と React Native のカレンダーで1枠ずつ頂くのではなく1人 Advent Calendar をやれば良かったなぁと12月に入ってから今更少し後悔しています。ただ、私は深く反省しているので、てへぺろ1つで許されることは目に見えています。張り切っていきましょう。
今回作るシステム
スーパーウルトラシンプルなサンプルアプリケーションを作ることにしましょう。今日やるべきタスクを雑に登録してひたすら消化するためだけのタスク管理システムを作りましょう。大人の都合で認証機能を設けた方が記事的に映えるのでログイン必須のシステムが良いですね。
/users/:id/tasks
を叩けば誰でもそのユーザーの今日のタスク状況が見られるようにしましょう。ユースケースとしては、まず各人は朝に今日中に片付けたいタスクを雑に列挙して(まあ朝起きられないおまえらは昼かもしれませんが)、各タスクが終わるたびにチェックを付けていき、プロジェクトマネージャーだか上司だかは URL を共有してもらっているので各人のタスク状況を web から確認できる、みたいな感じにしますか。
それ Google スプレッドシートでできるよ。web アプリケーションでできること
- サインアップ
- ログイン
- タスクの CRUD
- 他人のタスクも閲覧のみ可能
モバイルアプリケーションでできること
- ログイン (サインアップは web でやれ)
- 自身のタスクの CRUD (他人のタスクは web で見ろ)
想定読者
記事を書いた動機は、最近 Ruby on Rails で作った web アプリを React Native アプリ対応しようと思ったら参考日本語記事が少なくて苦労したためですが、この記事としては Ruby on Rails の心得が少しだけある初学者レベルを想定して書いています。Ruby on Rails 自体が初めての方も、写経で雑に理解しながら進めていくことはできるようには少しだけ配慮していますが、あまりにも初歩的で詳細すぎる説明は避けるようにも配慮しています。本記事(webアプリ編)だけでも、ちょっとした Ruby on Rails アプリケーションを作ることができるので、初学者の方は挑戦してみてほしいなぁ...という下心があります。
もし、この記事を進めていく上で躓いた場合や理解できないことがあった場合は @OgiharaRyo までご連絡頂くか、現在300人ほど参加している技術質問 slack コミュ二ティを運営していますので、こちらで質問して頂ければと思います。(slack 招待リンク)
環境
2019年12月初旬執筆時点の最新版を使用します。
% ruby -v ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-darwin19] % rails -v Rails 6.0.1 % psql -V psql (PostgreSQL) 12.1謝辞
以下のツイートで記事のテストプレイヤーを募集したところ、にしこさんとnonoさんにお声がけ頂き、記事中の不備や手順の漏れを確認及び指摘して頂きました。ありがとうございました。
[ゆるぼ]
— Ryo (@OgiharaRyo) December 6, 2019
Ruby on Rails と React Native でそれぞれサンプルアプリケーションを写経で作る記事を書いているのですが、その Ruby on Rails 編を明日明後日でテストプレイしてくださる方はいらっしゃいませんか? (続)Ruby on Rails アプリケーションの開発
rails new
オプション欲張りセットの
rails new
を実行します。本記事では web アプリケーションを Heroku で公開するため、データベースに PostgreSQL を指定しています。その他のたっぷりあるオプションは今回の記事に関係のないものを全て排除しています。ちなみにアプリケーション名にzone
と入っているのは、集中して没頭している意識状態になることを「ゾーンに入る」と表現することがあるので、そこから拝借しました。日次でタスクを可視化して消化するだけで、結構集中状態に入りやすくなる気がしませんか?まあそんなことは良いので早くrails new
してください。Ruby on Rails アプリケーション開発の始まりです!% rails new zone-web --database=postgresql --skip-action-mailer --skip-action-text --skip-active-storage --skip-action-cable --skip-javascript --skip-turbolinks --skip-testデータベースを作ってから一応起動を確認しておきましょう。
% cd zone-web % rails db:create % rails s
環境にもよりますが、大人の都合で以下よりあなたの開発サーバーが localhost:3000 であることを前提にリンクを記述していきますので異なる方は適時読み替えてください。ということで、localhost:3000 にアクセスしてご機嫌な表示がされたら ok です。
モデリング
とりあえず
User
モデルとTask
モデルを作成します。User
列 型 説明 name string ユーザーの名前を保存します。 account_id string 今回はメール認証等は行わないので、とりあえず任意のアカウントID(文字列)とパスワードのペアでログインできるようにします。ここをメールアドレスにしてメール認証のプロセスを挟んでも良いでしょう。 password_digest string ハッシュ化されたパスワードを保存します。このパスワードは復号できません。つまり、ユーザーがどんなパスワードを入力したのかはシステムが後から知ることはできません。 Task
列 型 説明 user_id string どのユーザーのタスクであるのかを保存します。 name string タスクの名前を入力します。見積もり時間も詳細も不要です。ただ1行完結なタスク内容が書ければ良いのです。日次でタスクを表示するので作成日も必要ですが、 rails g(enerate)
で生成されたマイグレーションファイルはデフォルトでcreated_at
という列にレコードの作成日時が入るようになるのでこれを利用します。ちなみにこれはあまり良い方法ではないので実務ではcreated_at
を業務上の都合と結び付けないようにしましょう。scaffold 等は行わずに Controller と View は温もりのあるお手製で作ります。モデルとマイグレーションファイルは冷たく Generator に頼ります。
% rails g model User name:string account_id:string password_digest:string % rails g model Task user:belongs_to name:string finished:boolean % rails db:migratemigrate した結果はこのようになっているはずです。
db/schema.rbcreate_table "tasks", force: :cascade do |t| t.bigint "user_id", null: false t.string "name" t.boolean "finished" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false t.index ["user_id"], name: "index_tasks_on_user_id" end create_table "users", force: :cascade do |t| t.string "name" t.string "account_id" t.string "password_digest" t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false end add_foreign_key "tasks", "users"
Task
モデルには generator がbelongs_to :user
を追加してくれていますが、User
側がTask
を複数持つことはモデルが知らないので一文追加しておきましょう。app/models/user.rbclass User < ApplicationRecord + has_many :tasks end
サインアップ
とりあえず
User
を作る機能を実装しなければ始まらないので、サインアップ機能を提供しましょう。サインアップフォーム表示
ルーティング
/signup
という URL にアクセスされたらUsersController
のnew
アクションが呼ばれるようにします。また、サインアップの submit で/signup
という URL に POST された時にUsersController
のcreate
アクションが呼ばれるようにします。config/routes.rbRails.application.routes.draw do + get 'signup', to: 'users#new' + post 'signup', to: 'users#create' end
Controller
新しく
app/controllers/users_controller.rb
を作成し、以下のコードを仮置きします。今回はnew
アクションでフォームが表示されることだけを目的とするので、create
アクションはまだ書きません。app/controllers/users_controller.rbclass UsersController < ApplicationController def new @user = User.new end endパスワードハッシュ化の準備
Gemfile でデフォルトでコメントアウトされている
bcrypt
をコメントインします。Gemfile- # gem 'bcrypt', '~> 3.1.7' + gem 'bcrypt', '~> 3.1.7'
bundle install
します。ちなみにbundle
だけ叩くとbundle install
されます。% bundle
User
モデルでhas_secure_password
をコールします。今インストールしたbcrypt
の力で、これをコールしておくことでモデルの属性にpassword
とpassword_confirmation(確認入力用)
が追加され、合致してsave
した時に自動でpassword_digest
にハッシュ化されたパスワードがセットされます。app/models/user.rbclass User < ApplicationRecord + has_secure_password end
View
新しく
app/views/users/new.html.erb
を作成し、ユーザー情報の入力フォームをマークアップしていきましょう。app/views/users/new.html.erb<%= form_with model: @user, url: signup_path, local: true do |f| %> <p> <%= f.label :name, 'お名前' %> <%= f.text_field :name %> </p> <p> <%= f.label :account_id, 'アカウントID' %> <%= f.text_field :account_id %> </p> <p> <%= f.label :password, 'パスワード' %> <%= f.password_field :password %> </p> <p> <%= f.label :password_confirmation, 'パスワード(確認)' %> <%= f.password_field :password_confirmation %> </p> <p><%= f.submit '送信' %></p> <% end %>これで localhost:3000/signup にアクセスするとフォームが表示されます。
サインアップ処理実装
今の状態で submit すると(送信ボタンを押すと) 「
create
アクションが見つからないんだが?!」というエラーで怒られます。初学者のあなたは一度 submit してみてエラー画面を目に焼き付けておきましょう。エラーメッセージを目に焼き付けた数が経験値になります。さて、早速 Controller にcreate
アクションを実装していきましょう。app/controllers/users_controller.rbclass UsersController < ApplicationController def new @user = User.new end + + def create + @user = User.new(user_params) + render :new unless @user.save + end + + private + + def user_params + params.require(:user).permit(:name, :account_id, :password, :password_confirmation) + end end
user_params
メソッドの中身は Strong Parameters といい、ブラウザーから POST されるはずのパラメーターのキーを個別に許可しています。これは、id
等といったサインアップフォームからは設定してほしくない属性(フォームにフィールドを置いていない属性)を特殊な方法で POST されてもデータベースに保存しないための防御機構です。ここで許可した属性以外は、users
テーブルにセットしないようにしているわけですね。続いて、作成後に「ご登録ありがとうございます。」とのテキストを表示できるようにします。新しく
app/views/users/create.html.erb
を作ってマークアップします。app/views/users/create.html.erb<p>ご登録ありがとうございます。</p>さて、実装できたら以下のような情報を入力してテストアカウントを作成してみましょう。
項目 入力する情報 お名前 テストアカウント アカウントID test パスワード password パスワード(確認) password 送信すると、先ほどの
create.html.erb
にマークアップしたテキストが表示されるはずです。上手く実装できていれば、
users
テーブルに1レコード作成されているはずです。rails console
から見てみましょう。% rails c Running via Spring preloader in process 48857 Loading development environment (Rails 6.0.1) irb(main):001:0>一番最近作られた
User
を参照します。このように先ほど作ったユーザーが表示されれば ok です。irb(main):001:0> User.last User Load (5.6ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT $1 [["LIMIT", 1]] => #<User id: 1, name: "テストアカウント", account_id: "test", password_digest: [FILTERED], created_at: "2019-12-01 11:58:46", updated_at: "2019-12-01 11:58:46">「ご登録ありがとうございます。」との表示を確認したのに
User
の情報が表示されずnil
が返ってくる場合はコードを見直してみてください。Strong Parameters あたりが怪しいと思います。異常処理
今のサインアップ機構には以下の問題点があります。
- 名前やアカウントIDやパスワードが空でも登録できてしまう
- パスワードとパスワード(確認)が違っている場合にユーザーに何も通知されない
- 同一のアカウントIDを登録できてしまう
同一のアカウントIDを登録できてしまうと、ログインする時に入力されたアカウントIDでどのユーザーで認証すれか良いか分からなくなるので、アカウントIDは既に登録されている値を使えなくする必要があります。これから、それらの問題を解決していきます。
サーバーサイドバリデーション
まず、Model 側でバリデーションを設定してレコードを作成する時に値の有効性を検証することにしましょう。パスワードを必須であるかや、パスワードとパスワード(確認)が一致しているかは、
has_secure_password
が検証してくれるので、残りの名前とアカウントIDに関するバリデーションを定義していきます。app/models/user.rbclass User < ApplicationRecord has_secure_password + + validates :name, presence: true + validates :account_id, presence: true, uniqueness: true end
これで、名前が必須となり、アカウントIDが必須かつ重複不可となりました。
rails console
で試してみましょう。名前を空にして、アカウントIDを先ほど画面から作ったtest
にして、パスワードとパスワード(確認)に別の文字列を入れてみます。irb(main):001:0> User.create!(name: '', account_id: 'test', password: 'password', password_confirmation: 'invalid') (0.1ms) BEGIN User Exists? (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."account_id" = $1 LIMIT $2 [["account_id", "test"], ["LIMIT", 1]] (0.3ms) ROLLBACK Traceback (most recent call last): 1: from (irb):2 ActiveRecord::RecordInvalid (Validation failed: Password confirmation doesn't match Password, Name can't be blank, Account has already been taken)今回重要なのは最終行です。
ActiveRecord::RecordInvalid (Validation failed: Password confirmation doesn't match Password, Name can't be blank, Account has already been taken)
と書かれていますね。つまり、パスワード(確認)がパスワードと合っていないよ、名前は空にできないよ、アカウントが重複しているよ、と言われています。問題なさそうですね。ちなみに勿論、データベースには保存されていません。不安であれば再び
User.last
で確かめてみましょう。ちなみに、エラーになる条件は他にもパターンがあります。アカウントIDやパスワードが空の場合です。勿論このままそういったパターンの検証を続けても良いですが、実際の開発ではこういった挙動確認はテストコードを書いて行うことが多いです。ただし、テストの話をすると私がうるさくなるので、本記事ではノーテストでいきます。「ノーテストで開発するのはノーヘルでバイクに乗るようなものです。つまり気持ちいいってことです。」みたいなことを誰か偉い人が言っていたような気がします。わかる。エラーメッセージの通知
さて、サーバーからバリデーションエラーで弾かれたことをユーザーに通知してやる必要があります。エラーメッセージは、
@user.errors.full_messages
で配列で取り出すことができます。View のform_with
ブロックの一番上にエラーメッセージ表示用のマークアップを行いましょう。app/views/users/new.html.erb<%= form_with model: @user, url: signup_path, local: true do |f| %> + <% if @user.errors.any? %> + <ul> + <% @user.errors.full_messages.each do |message| %> + <li><%= message %></li> + <% end %> + </ul> + <% end %> ... <% end %>
これで、
@user
インスタンスがエラー情報を持っている場合のみバリデーションエラーのリストを表示することができます。早速動かしてみましょう。先ほどrails c
で試してみたことをブラウザーから試してみましょう。以下のスクリーンショットのように、名前を空にして、アカウントIDを先ほど画面から作ったtest
にして、パスワードとパスワード(確認)に別の文字列を入れてみます。この状態で送信ボタンを押して submit すると以下のようにエラーメッセージが表示されます。先ほど
rails c
で確認したエラーメッセージと同様ですね。ちなみにこのメッセージを日本語にすることもできるのですが、少しコードが散らかって今回作りたいシステムの目的から脱線するので今回は英語のままにしておきます。変なところで投げっぱなし、それがこの記事の雑さです。また、上記2つのスクリーンショットを見比べてみると、ラベルと入力フィールドの間の改行状態が変わっていることが分かります。これは Ruby on Rails (
f.text_field
)が気を利かせて生成される HTML にエラーであることが分かるような手を加えてくれているのですが、ここで詳しくは解説しません。気になる方は、ブラウザーのデベロッパーツールから生成された HTML のソースコードを読んでみてください。変なところで投げっぱなし、それがこの記事の雑さです。クライアントバリデーション
さて、ついでにクライアント(ブラウザー)側でも簡易的なバリデーションを設定しておきましょう。今回はとても簡単な「フィールドを必須にする」という実装だけを行います。なんと、HTML5 の
input
要素はrequired
属性を与えるだけでそのフィールドが必須であることをブラウザーに指示してくれます。Ruby のフォームビルダーのメソッド(f.text_field
等)でrequired
属性を指定するにはrequired: true
というオプションを与えてやるだけで ok です。app/views/users/new.html.erb<p> <%= f.label :name, 'お名前' %> - <%= f.text_field :name %> + <%= f.text_field :name, required: true %> </p> <p> <%= f.label :account_id, 'アカウントID' %> - <%= f.text_field :account_id %> + <%= f.text_field :account_id, required: true %> </p> <p> <%= f.label :password, 'パスワード' %> - <%= f.password_field :password %> + <%= f.password_field :password, required: true %> </p> <p> <%= f.label :password_confirmation, 'パスワード(確認)' %> - <%= f.password_field :password_confirmation, required: true %> + <%= f.password_field :password_confirmation, required: true %> </p>この状態でフィールドを空にして submit しようとすると、ブラウザーから以下のスクリーンショットのように警告が表示されてキャンセルされると思います。このスクリーンショットは macOS の Chrome です。
「どの道サーバーで弾かれてエラーメッセージも表示されるんだからクライアント側のバリデーションは冗長では?」と思う方もいらっしゃるかもしれませんが、サーバーに問い合わせなくても入力段階で弾けるものは弾いてしまっておいた方が、ユーザーの手間を煩わせずに済むことが多いため、UX の観点からクライアント側でもバリデーションを行うことが多いです。変なところでこだわる、それがこの記事の雑さです。
スタイリング
無事にサインアップの機能を作ることができましたが、フォームがあまりにも簡素で気分が上がらないので少しだけ見た目に気を使うことにしましょう。ただ、この記事では「CSS 絶対に書きたくないでござる!」という気持ちと、「CSS フレームワークに依存した class 付けは絶対にしたくないでござる!」という気持ちと、「CSS フレームワークの導入は CDN だけで終わらせたいでござる!」という気持ちが強いので、CSS を書かなくても class を付けなくても良くて CDN で読み込むだけでそれっぽい見た目になる方法を探します。そこで mini.css を利用します。記事執筆時点で、ここまでに書いたサインアップフォームを一切触らなくても CDN へのリンクを書くだけでスタイルが当たるものを探し回ったところ mini.css が一番それっぽくなったような気がしました。ということで、早速 CDN から mini.css を読み込みます。たった1行書くだけで CSS を丸ごと配信してもらえるなんて至れり尽くせりですね。また、mini.css の ドキュメント(Getting started) を見ると、「viewport はちゃんと指定しておけよ」と書いてあるので従います。各ページ共通のレイアウト(
head
要素等) はapp/views/layouts/application.html.erb
に書かれているので、ここに viewport の指定と CDN へのリンクを設定します。app/views/layouts/application.html.erb<head> <title>ZoneWeb</title> <%= csrf_meta_tags %> <%= csp_meta_tag %> + <meta name="viewport" content="width=device-width, initial-scale=1"> + + <link rel="stylesheet" href="https://cdn.rawgit.com/Chalarangelo/mini.css/v3.0.1/dist/mini-default.min.css"> <%= stylesheet_link_tag 'application', media: 'all' %> </head>
この状態でサインアップフォームにアクセスすると、それっぽいデザインになっています。
required
属性が付いたinput
要素のフィールドが未入力だと赤枠で囲まれるのが素敵ですね。まあいろいろと、もう少し何とかしたい感はありますが、一切の class 付けもカスタマイズも行わずにただ CSS を読み込むだけで、ここまでスタイリングしてもらえれば充分です。今回はスタイリングのことは考えずに進めたいので、一旦はこのまま進もうと思います。
認証
次は、サインアップしたユーザーのアカウントIDとパスワードでログインできるようにします。この章では以下のような仕様で実装していきます。
- ログインしたらトップページへ遷移
- ログインしていない状態でトップページへアクセスされたらログインページへリダイレクト
トップページ
まず、ログインした時に表示するためのトップページを作ります。トップページはタスクの一覧をいきなり表示するので、
TasksController
のindex
アクションにルーティングします。config/routes.rbRails.application.routes.draw do + root 'tasks#index' get 'signup', to: 'users#new' post 'signup', to: 'users#create' end
app/controllers/tasks_controller.rb
を作成し、TasksController
のindex
アクションを仮実装します。今は、ログイン後にページが表示されれば何でも良いので空っぽのアクションで ok です。app/controllers/tasks_controller.rbclass TasksController < ApplicationController def index; end end続いて
app/views/tasks/index.html.erb
を作成し、View を仮実装します。テンションの上がりそうなテキストを置いておきましょう。app/views/tasks/index.html.erb<p>Welcome!!</p>これで、トップページ(localhost:3000)にアクセスするとテンションが上がります。
ログインフォーム実装
サインアップページやトップページを作った時と同様に、サクサクとルーティングと Controller と View を実装していきましょう。少しずつ慣れてきましたか?
config/routes.rbRails.application.routes.draw do root 'tasks#index' get 'signup', to: 'users#new' post 'signup', to: 'users#create' + + get 'login', to: 'sessions#new' + post 'login', to: 'sessions#create' end
app/controllers/sessions_controller.rbclass SessionsController < ApplicationController def new; end def create; end endapp/views/sessions/new.html.erb<%= form_with scope: :session, url: login_path, local: true do |f| %> <p> <%= f.label :account_id, 'アカウントID' %> <%= f.text_field :account_id, required: true %> </p> <p> <%= f.label :password, 'パスワード' %> <%= f.password_field :password, required: true %> </p> <p><%= f.submit 'ログイン' %></p> <% end %> <p><%= link_to 'サインアップはこちら', signup_path %></p>サクサクと実装できましたか?これでログインページ(localhost:3000/login)にアクセスするとフォームが表示されます。
あとは、サインアップ完了画面にログインリンクを追加しておきましょう。
app/views/users/create.html.erb<p>ご登録ありがとうございます。</p> + <p><%= link_to 'ログインページ', login_path %>からログインしてご利用を開始してください。</p>
認証
さて、それではログイン処理を実装していきましょう。いきなり難易度が跳ね上がりますが、あれだったらコピペして後は見なかったことにして切り抜けてください。まずは認証のコア部分を書いていきます。今回は超シンプルな認証にするので
devise
等の Gem は使わずに remember me も実装せず、大したセキュリティも意識せずにサクッとお手製で作っていきます。認証系を手書きするのは地獄だと思われるかもしれませんが、これだけです。app/helpers/sessions_helper.rbmodule SessionsHelper def login(user) session[:user_id] = user.id end def current_user @current_user ||= User.find_by(id: session[:user_id]) end def logged_in? current_user.present? end end
login
メソッドはただの代入のように見えますが、これはUser
の ID をクライアントの Cookie に暗号化して詰めています。この Cookie の中身を復号した値は代入時と同じようにsession
メソッドから呼び出せます。つまり、session[:user_id]
にUser
の ID を詰めておけば、次回以降のリクエストでユーザーに Cookie が残っていればsession[:user_id]
を参照することで読み出すことができます。
current_user
メソッドは、上述のsession[:user_id]
を使ってUser
のインスタンスを取得して返します。User.find_by(id: session[:user_id])
で Cookie に入っているユーザーの ID でデータベースを照会している訳ですね。||=
という演算子の使い方は見慣れない方もいるでしょう。これは、演算子の短絡評価の特性を使って左辺が偽の場合のみ代入が実行するための文法です。論理和演算(||
)における短絡評価とは、「左辺が偽なら右辺が真であろうが偽であろうがどうせ偽になるので右辺をそもそも評価する必要ないよね」という評価法です。左辺が真であって初めて右辺を気にするということですね。ここで||=
を使う目的は、同じリクエスト内で複数回current_user
メソッドがコールされた時のデータベースアクセスを初回以外スキップすることです。つまり初回でデータベースからUser
のインスタンスを引っ張ってきたら、次回以降はインスタンス変数を参照することでデータベースにアクセスするコストを防いでいるということです。
logged_in?
メソッドは、見ての通りcurrent_user
でUser
のインスタンスを引っ張れたかどうかを確認するメソッドです。find_by
は見つからなかった時にnil
を返すので、session[:user_id]
が空の場合や存在しないユーザーの ID 等が入っている場合は、logged_in?
メソッドがfalse
を返すということです。さて、ログイン機構を作ったところで、ログインフォームから POST されてきた時の処理を実装しましょう。
SessionsController
のcreate
アクションを実装していきます。app/controllers/sessions_controller.rbdef create user = User.find_by(account_id: params[:session][:account_id]) if user&.authenticate(params[:session][:password]) login user redirect_to root_path else flash.now[:alert] = 'アカウントID、またはパスワードが間違っています。' render :new end endユーザーから POST された値(ログインフォームに入力されたアカウントIDとパスワード)は、
params[:session]
に入っているので、まずUser.find_by(account_id: params[:session][:account_id])
でそのアカウントIDのユーザーを引っ張り、次行のif user&.authenticate(params[:session][:password])
でパスワードが正しいかを確認しています。authenticate
メソッドはUser
モデルでコールしたhas_secure_password
メソッドによって提供されていて、引数に与えたパスワードが正しいかどうかを返してくれます。&.
という演算子も見慣れない方がいるかもしれません。これは Safe Navigation Operator という演算子で、レシーバーが nil であればNoMethodError
を投げずにnil
を返してくれます。昔の Ruby を知っている方はuser.try!(:authenticate, params[:session][:password])
と同じだと考えると良いかもしれません。つまり、ユーザーが存在しないアカウントIDを POST してきてfind_by
でUser
のインスタンスが引っ張れずにnil
を返してきた時に、else
に入るために利用しています。if user.present? && user.authenticate(params[:session][:password])
という上述の演算子の短絡評価を使った書き方の方が明示的で好みな方もいるかもしれませんね。余談ですが、&
はひとりぼっちで体育座りしている様子に見えるため&.
は通称「ぼっちオペレーター」と呼ばれていたりします。そんなこんなで認証できた場合は
if
が真となるので、先ほどSessionsHelper
に定義したlogin
メソッドをコールして Cookie にUser
の ID を詰めてトップページにリダイレクトしています。else
に入った場合は、POST されたアカウントIDが存在していなかったか、アカウントIDは存在していたけどパスワードが間違っていたかのいずれかなので、エラーメッセージをflash
に詰めてnew.html.erb
を再描画します。詰めたエラーメッセージを表示する処理は後ほど書きましょう。その前に、今のままだとSessionsHelper
のlogin
メソッドがSessionsController
からは見えていない問題を解決します。SessionsHelper
のcurrent_user
はあちこちで使うことになるので全ての Controller が継承しているApplicationController
で Mix-in します。app/controllers/application_controller.rbclass ApplicationController < ActionController::Base + include SessionsHelper end
これで、
ApplicationController
を継承している全ての Controller でSessionsHelper
のcurrent_user
やlogged_in?
を利用できるようになりました。Helper を Controller に Mix-in するのって気持ち悪いですが、現状スッキリとした方法が思い浮かばないのでこのままいきます。そして、ログインページに、ログインに失敗した時のエラーメッセージを表示します。先ほど Controller の
flash.now[:alert]
で詰めたエラーメッセージが表示されます。if flash[:alert].present?
しても良いですが、まあflash[:alert]
の中身がnil
ならどうせ何も出ないので雑に一行置いておきましょう。app/views/sessions/new.html.erb<%= form_with scope: :session, url: login_path, local: true do |f| %> + <%= flash[:alert] %> <p> <%= f.label :account_id, 'アカウントID' %> <%= f.text_field :account_id, required: true %> </p> ... <% end %>
最後に、ログインに成功してトップページにリダイレクトされた後、本当にログインに成功しているのかを確かめるために「Welcome!!」 の文字列を「Welcome [ユーザーの名前(
User#name
)]!!」に変更しましょう。app/views/tasks/index.html.erb- <p>Welcome!!</p> + <p>Welcome <%= current_user.name %>!!</p>これでログイン処理周りの実装は完了です。早速動かしてみましょう!まずは、適当なアカウントIDやパスワードを入力してログインに失敗した場合にエラーメッセージが表示されるかを確認しましょう。
次に、正しいアカウントIDとパスワードでログインしてみましょう。先ほど作ったテストアカウント(アカウントID:
test
, パスワード:password
) でログインしてみましょう。いろいろあってテストアカウントがない場合はサインアップページ(localhost:3000/signup)から作りましょう。ログインに成功してユーザー名が表示されれば ok です。
ログアウト
ログインできるようになったのでログアウトも実装しましょう。サクサクといきましょう。まずはルーティングを追加します。
/login
に対する DELETE リクエストをSessionsController
のdestroy
アクションにルーティングします。config/routes.rbget 'login', to: 'sessions#new' post 'login', to: 'sessions#create' + delete 'logout', to: 'sessions#destroy'
SessionsController
のdestroy
を実装していきます。この後追加するSessionsHelper
のlogout
メソッドをコールしてログインページにリダイレクトします。app/controllers/sessions_controller.rbdef create ... end + + def destroy + logout + redirect_to login_path + end end
そして、実際のログアウト処理です。Cookie から
User
の ID 情報を削除して、インスタンス変数もnil
で初期化するだけです。app/helpers/sessions_helper.rbdef logged_in? current_user.present? end + + def logout + session.delete(:user_id) + @current_user = nil + end end
最後にログアウトのリンクを置きます。まあとりあえずトップページにでも置いておきましょう。
app/views/tasks/index.html.erb<p>Welcome <%= current_user.name %>!!</p> + + <%= link_to 'ログアウト', logout_path, method: :delete %>
最後と言ったな、あれは嘘だ。まだ大きな問題があります。上記のコードでは
link_to
にmethod
オプションを与えることで、生成されるa
要素にdata-method="delete"
属性を追加して GET ではなく DELETE でリクエストを送ることを期待しています。しかし、現状はそんなことはできずに GET リクエストで/logout
にリクエストされてルーティングエラーになります。「えっ?よくやってるけど」と思う方もいらっしゃるかもしれませんが、これは jquery-ujs が提供している機能です。今回は--skip-javascript
でrails new
したため jquery-ujs なんて高級なものは入っていません。よし、CDN から読み込みましょう。(雑)app/views/layouts/application.html.erb<link rel="stylesheet" href="https://cdn.rawgit.com/Chalarangelo/mini.css/v3.0.1/dist/mini-default.min.css"> <%= stylesheet_link_tag 'application', media: 'all' %> + + <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-ujs/1.2.2/rails.js"></script> </head>
ちなみに
data-method
属性を持ったa
要素で任意のリクエストを投げるためのコードはこれです。なんと、a
要素が押された瞬間にdata-method
に従ったメソッドでリクエストを投げられるようなform
要素を錬金して強制submit
しています。最初からform
を組み立てておいても良いならlink_to
ではなくbutton_to
を使う方法もあるのですがデザインがいい感じにならなかったので jquery-ujs を読み込んじゃうことにしました、てへぺろ。まあ今回はしばらく使い古した web アプリケーションのモバイルアプリ化が目的なので、ちょっと jQuery ぐらい置いてたり、設計が歪だったりした方がリアルじゃないですか。ちなみにrails-ujs
の話がタイムリーなことに別の Advent Calendar の記事で行われていたので、詳しく知りたい方はこちらの記事をどうぞ。さて、ということで、ログアウト処理が実装できました。ログインしてトップページからログアウトリンクを踏むとログインページに戻されるためです。ブラウザーのデベロッパーツールでログアウト前とログアウト後で Cookie がどのような動きをするかを確認してみても良いかもしれませんね。
ログイン必須化
ログインとログアウトの処理は実装しましたが、もう1つだけ問題があります。それは、ログアウトした状態であってもトップページにアクセスできてしまうことです。ログインしていない状態で localhost:3000 にアクセスしてみましょう。このように「
current_user
がnil
だからname
なんかにはアクセスできませんぞ!!」というエラーが表示されます。ログアウト状態では、
session[:user_id]
が空なのでcurrent_user
はnil
を返すのでしたね、覚えていますか?回避方法としては先ほどの Safe Navigation Operator (通称ぼっちオペレーター)を使ってcurrent_user&.name
にする手もありますが、そもそも今回の要件としては、トップページはログインユーザー以外アクセスできてはならないということなので、未ログインのユーザーがトップページにアクセスしてきたらログインページにリダイレクトするようにしましょう。app/controllers/tasks_controller.rbclass TasksController < ApplicationController + before_action :login_required def index; end + + private + + def login_required + redirect_to login_path unless logged_in? + end end
before_action
にシンボルで渡したメソッドは、この Controller のアクションがコールされる前に実行されます。ここでは、unless logged_in?
の時、つまり未ログインの時にログインページにリダイレクトしています。では、ログアウトした状態でトップページにアクセスしてみましょう。ログインページにリダイレクトされており、ログインしたらトップページが表示されれば ok です。あとは肝心のタスク管理ができるようになれば web アプリケーションとしては完成です。タスクの CRUD
CRUD とは、データの Create, Read, Update, Delete の頭文字を取ったものです。つまり、タスクを作ったり読み出したり更新したり消したりといったコードをこれから書いていきます。今回は Delete は実装しないので、CRU かもしれませんね。
一覧ページと作成フォーム
タスクの一覧ページはトップページとユーザーIDを指定したページの2種類用意します。つまり、localhost:3000 と localhost:3000/users/:id/tasks の2種類です。
:id
の部分にはそのUser
のid
属性が入ります。また、タスクの一覧ページに正しくタスクが表示されているかを確認するために、一気にタスク作成フォームまで作ってしまうのでcreate
アクションも実装します。今回はindex
ページにタスク追加フォームを配置するのでnew
の専用フォームはありません。index
とcreate
を同時に実装していくのは難しそうに感じるかもしれませんが、大丈夫、あなたは写経するだけです。config/routes.rbget 'login', to: 'sessions#new' post 'login', to: 'sessions#create' delete 'logout', to: 'sessions#destroy' + + resources :users, only: [] do + resources :tasks, only: :index + end + resources :tasks, only: :create
それでは、タスクの一覧を表示するための Controller 処理を書いていきます。今回少し癖があるのは、
/users/:id/tasks
の URL でアクセスされた場合は:id
で指定されたUser
のタスクを取得しますが、そうでない場合はログイン中のUser(current_user)
のタスクを取得する必要があるということです。app/controllers/tasks_controller.rbclass TasksController < ApplicationController before_action :login_required + before_action :set_user - def index; end + def index + @tasks = @user.tasks.where(created_at: Time.zone.today.all_day) + end + + def create + @task = Task.create(task_params.merge(user: current_user)) + redirect_to Rails.application.routes.recognize_path(request.referer) + end private def login_required redirect_to login_path unless logged_in? end + + def set_user + @user = (params[:user_id].present? ? User.find(params[:user_id]) : current_user) + end + + def task_params + params.require(:task).permit(:name, :finished) + end endまずは
before_action
で今回タスクを表示するUser
のインスタンスを@user
に代入しています。もし、params[:user_id]
が存在しているのであれば、/users/:id/tasks
からのリクエストなのでUser.find(params[:user_id])
で対象のUser
のインスタンスを取得します。params[:id]
が存在していなければトップページからのリクエストなのでcurrent_user
を取得します。index
アクションでは、この@user
が持っている今日付けのタスクを全て取得しています。create
アクションでは、飛んできたパラメーターにログインユーザーのuser_id
をセットしてtasks
テーブルのレコードを作成し、元いたページ(トップページ or/users/:id/tasks
)にリダイレクトしています。次に View を書いていきましょう。Welcome メッセージとログアウトリンクだけであったページを、タスク一覧とタスク作成フォームの表示ページとしてマークアップしていきます。
app/views/tasks/index.html.erb<p><%= @user.name %></p> <% if @user == current_user %> <%= form_with model: Task.new, local: true do |f| %> <%= f.text_field :name, required: true %> <%= f.submit '作成' %> <% end %> <% end %> <ul> <% @tasks.each do |task| %> <li> <%= check_box_tag "finished#{task.id}", true, task.finished, data: { id: task.id }, disabled: (@user != current_user) %> <%= label_tag "finished#{task.id}", task.name %> </li> <% end %> </ul> <%= link_to 'ログアウト', logout_path, method: :delete %> <% if @user == current_user %> <script> $(document).ready(function(){ $('#task_name').focus(); }); </script> <% end %> <style> li { list-style: none; } :checked + label { color: gray; text-decoration: line-through; } </style>今回のアプリケーションでは、タスクのチェックボックスにチェックを入れた状態を完了状態とします。ただし、人のタスクを勝手に完了にするわけにはいかないので、
disabled: (@user != current_user)
で今見ているページが自分のページでない時に限りチェックボックスを非活性にしています。チェックボックスをチェックした時にサーバーにリクエストを送ってfinished
を変更する処理は後ほど書いていきます。今はただチェックボックスを表示するだけのコードです。check_box_tag
の各種引数は覚えるかググるかしかないので、まあ適当に察してください。フォームヘルパーの引数順は何百回ググっても覚えられませんね。まあ、都度ググれば良いのです。チェックボックスの活性状態と同様に、タスクの新規作成フォームもif @user == current_user
で今見ているページが自分のページである場合のみ表示しています。今回の View には純粋な HTML の文書構造だけではなく、
script
要素とstyle
要素を使っています。script
要素では、JavaScript でページを表示した時にタスク名の入力フォームにフォーカスが当たるようにしています。こうすることで、タスク名を打って return キーを叩く操作を繰り返すだけで次々にタスクを作っていくことができて快適になります。ここでも jQuery を利用しています。style
要素では軽くデザインを当てていて、li
要素のlist-style: none;
で箇条書きの・
を非表示にして、チェックされたチェックボックスの後続のlabel
要素のテキストカラーをグレーにして取り消し線を付けています。まあどちらも触ってみれば効果が分かるでしょう。ということで動作確認です。少し複雑な仕様で作り込んだのでテストも複雑です。テストコードを書いた方が早くテストできるのでストレスは溜まりますが、一生懸命ブラウザーで手動確認していきましょう。準備として、別のユーザーをサインアップフォームから作成しておいてください。サインアップを何度か繰り返していて、もう今のデータベースの状態がよく分からなくなってしまった方は、一度データベースをリセットしてゼロから作っていきましょう。
% rails db:migrate:reset % rails c irb(main):001:0> User.create(name: 'テストアカウント', account_id: 'test', password: 'password', password_confirmation: 'password') irb(main):002:0> User.create(name: 'テストアカウント2', account_id: 'test2', password: 'password', password_confirmation: 'password') irb(main):003:0> exit2ユーザーをシステムに作成できたらログインとログアウトを繰り返しながら双方のタスクをいくつか作ってみて、以下を確認してみましょう。
- トップページ(localhost:3000)
- 自分のタスクが表示されること
- タスクの作成が行えること
- チェックボックスをチェックできること
- 自分のタスク一覧ページ(localhost:3000/users/1/tasks)
- 自分のタスクが表示されること
- タスクの作成が行えること
- チェックボックスをチェックできること
- 他人のタスク一覧ページ(localhost:3000/users/2/tasks)
- 他人のタスクが表示されること
- タスク作成フォームが表示されていないこと
- チェックボックスをチェックできないこと
タスクの更新
チェックボックスをチェックすることで、
tasks
テーブルのfinished
列を更新します。今回はチェックボックスの状態変更のイベントを拾って Ajax で非同期にサーバーにステータス更新リクエストを投げることにします。まずはルーティングにupdate
アクションを追加します。config/routes.rbresources :users, only: [] do resources :tasks, only: :index end - resources :tasks, only: :create + resources :tasks, only: %i[create update]次に
TasksController
のcreate
アクションの下にupdate
アクションを追加しましょう。上述のルーティングによって/tasks/:id
という URL でリクエストが送られてくるので、params[:id]
のTask
を更新します。ただし、更新しようとしているタスクがログインしているユーザーのタスクであった場合のみ更新を行い、ログインしているユーザーのタスクでなかった場合はログインページにリダイレクトしています。勝手に他人のタスクを弄られないようにする配慮です。app/controllers/tasks_controllerdef update task = Task.find(params[:id]) redirect_to login_path if task.user != current_user task.update(task_params) endそして、View の
script
要素内にチェックボックスの状態が変化した時に上記の Controller までリクエストを投げる処理を追加します。app/views/tasks/index.html.erb<script> $(document).ready(function(){ $('#task_name').focus(); }); + + $('input[type="checkbox"]').change(function() { + $.ajax('/tasks/' + $(this).data('id'), + { + type: 'patch', + data: { + task: { + finished: $(this).prop('checked') + } + }, + dataType: 'json' + } + ) + }); </script>
チェックボックスを触る度にページをリロードされてはストレスなので、Ajax で非同期にリクエストを投げることでページ遷移を挟まずにリクエストを投げています。やや複雑に見えるコードですが、落ち着いて1行ずつ読んでみれば意味は分かるはずです。
$('input[type="checkbox"]').change
はチェックボックスが変更された場合イベントを拾おうとしています。$.ajax('/tasks/' + $(this).data('id')
では Ajax でリクエストを送る URL を組み立てています。変更されたチェックボックスである$(this)
のdata-id
属性を使って/tasks/1
といった URL を作っています。data-id
属性についてピンと来なければcheck_box_tag
にどんなパラメーターを与えたかと、結果としてどのような HTML が生成されているかをブラウザーからソースコードを見て確認してみましょう。type: 'patch'
は PATCH リクエストを送るためです。PATCH がピンと来なければrails routes
コマンドを実行してconfig/routes.rb
によってどのような URL とメソッドがアプリケーションに定義されているかを確認してみましょう。data: { task: { finished: $(this).prop('checked') } }
はサーバーに送るパラメーターです。動作確認する時にチェックボックスを操作した時にrails s
に出るログを確認してチェックを付けた時と外した時でそれぞれどんなパラメーターが飛んできているかを確認してみましょう。ということで、早速動かしてみましょう。チェック状態がページをリロードしても変わらなければ ok です。なんと、これで web アプリケーションとしては完成です。日次のタスクをひたすら登録して消化していくには充分な機能ができました。勿論、まだまだ改善の余地はあるのでスタイリングをするなり、ユーザーのお気に入り機能を付けるなり、他人のタスクを見るだけならログイン必須ではなくしたり、いろいろ取り組んでみると力が付くかもしれませんね。とりあえず、本記事で作りたかった web アプリケーションはこれで完成です。お疲れ様でした。
モバイルアプリケーション用 API 実装
ここからは、モバイルアプリケーションからこの web アプリケーションを扱えるように API を実装していきます。外部からでもタスクの情報を取得したり、タスクの完了状態を変更したりできるようにするためのインターフェースを提供するということですね。早速やっていきましょう。
認証
今作った web アプリケーションでは、タスクを見るにも作るにも更新するにもアカウントIDとパスワードによるログイン認証が必要でした。モバイルアプリケーションも同様で、正しいアカウントIDとパスワードを入力したモバイルアプリケーションにのみタスクの操作を許してあげる必要があります。web アプリケーションは、モバイルアプリケーションが正しいアカウントIDとパスワードを送ってきた場合は API トークンを返します。web アプリケーションは、モバイルアプリケーションがパラメーターに乗せてきた API トークンでログインユーザーを判別します。何を言っているのか分からないと思いますので、実装を始めてしまいましょう。作っていくうちに何となく分かってくるはずです。
APIトークンの発行
まずは
users
テーブルにapi_token
の列を追加します。% rails g migration AddApiTokenToUsers api_token:string % rails db:migrateそして、
users
テーブルにレコードを追加する時に自動でAPIトークンを埋めるようにします。何と1行追加するだけでbcrypt
が勝手にやってくれます。app/models/user.rbclass User < ApplicationRecord has_many :tasks has_secure_password + has_secure_token :api_token validates :name, presence: true validates :account_id, presence: true, uniqueness: true end
これで新規の
User
に関しては自動でapi_token
がセットされるようになりましたが、既に存在するusers
テーブルのレコードのapi_token
は空のままなのでrails console
からワンライナーで一気に発行してしまいましょう。ちゃんと発行されたか不安であればUser.first.api_token
等で確認してみましょう。これで API トークンの発行準備は完了です。% rails c irb(main):001:0> User.find_each { |user| user.regenerate_api_token }ログイン
次に、ログイン用のルーティングを追加します。API に関する URL は必ず
/api
で始めることにしましょう。また、API 用の Controller はApi
という名前空間で括ることにしましょう。つまり、ログイン API は URL が/api/login
で Controller はapp/controllers/api/login_controller.rb
にあるApi::LoginController
クラスとなります。config/routes.rbresources :tasks, only: %i[create update] + + namespace :api do + get 'login', to: 'login#show' + end
次に Controller を実装していきます。
app/controllers/api/login_controller.rbclass Api::LoginController < ApplicationController def show @user = User.find_by(account_id: params[:account_id]) if @user&.authenticate(params[:password]) render status: :ok, json: { api_token: @user.api_token } else render status: :unauthorized, json: {} end end endそれでは、localhost:3000/api/login?account_id=test&password=password にアクセスしてみましょう。このような API トークンが表示されれば ok です。
また、パラメーターに誤ったアカウントIDかパスワードを指定してみましょう。この時は API トークンは返ってきません。
タスク CRUD API
認証と一覧の取得
自身のタスクの一覧を返したり、更新させたりという機能を書いていくわけですが、それを要求してきたユーザーが上記の API トークンをちゃんと持っているかを確認する必要があります。ルールとして、リクエストのパラメーターに必ず
api_token
を与えてもらうことにします。api_token
のパラメーターが渡されていない場合、あるいは誤っている場合のリクエストは無視します。app/controllers/api/tasks_controller.rbclass Api::TasksController < ApplicationController before_action :authenticate_by_token def index @tasks = @user.tasks.where(created_at: Time.zone.today.all_day) render json: @tasks.map { |task| { id: task.id, name: task.name, finished: task.finished } } end private def authenticate_by_token @user = User.find_by(api_token: params[:api_token]) render status: :unauthorized, json: 'Invalid API token' if @user.blank? end end
before_action
では、トークンの認証を行い失敗した場合にInvalid API token
という文字列とステータスコード 401 を返します。正しい API トークンを与えた場合はタスクの一覧の json が返ってきます。これが
@tasks.map { |task| { name: task.name, finished: task.finished } }
の結果です。モバイルアプリケーションは、この情報を元にタスクの一覧画面を組み立てるわけですね。作成と更新
続いて作成と更新の API を一気に書いていきます。やることは web アプリケーションとほとんど同じなのでサクサクといきましょう。まずはルーティングに
create
とupdate
アクションを追加します。config/routes.rbnamespace :api do get 'login', to: 'login#show' - resources :tasks, only: :index + resources :tasks, only: %i[index create update] endそして、Controller に web の時と同様に
create
とupdate
アクションを実装していきます。app/controllers/api/tasks_controller.rbclass Api::TasksController < ApplicationController + skip_forgery_protection before_action :authenticate_by_token def index @tasks = @user.tasks.where(created_at: Time.zone.today.all_day) render json: @tasks.map { |task| { id: task.id, name: task.name, finished: task.finished } } end + + def create + task = Task.create(task_params.merge(user: @user)) + render json: { id: task.id, name: task.name, finished: task.finished }, status: :created + end + + def update + task = Task.find(params[:id]) + (task.user == @user) ? task.update(task_params) : render(status: :unauthorized) + end private def authenticate_by_token @user = User.find_by(api_token: params[:api_token]) render status: :unauthorized, json: 'Invalid API token' if @user.blank? end + + def task_params + params.require(:task).permit(:name, :finished) + end end
create
アクションとupdate
アクションの中身はほとんど前回と同じなので問題ないでしょう。問題があるとすれば先頭にしれっと追加されたskip_forgery_protection
という一行です。これは話すと長くなるのですが、Ruby on Rails はデフォルトで CSRF(Cross-Site Request Forgeries) 保護が行われています。外部の攻撃者から不正なリクエストを受けても弾けるように、POST や PATCH リクエストは認証トークンを添えて送らなければリクエストを処理しません。認証トークンで認証できない場合は Controller のbefore_action
時点で弾かれるのでcreate
やupdate
といったアクションに入ってくることはありません。skip_forgery_protection
はこれをskip_before_action
するためのものです。この後curl
で API を叩いてみるので興味のある方はskip_forgery_protection
をコメントアウトした状態で API を叩いてrails server
のログを確認してみると良いでしょう。Can't verify CSRF token authenticity.
というエラーが表示されているはずです。そして作成や更新は行われずにデータベースに変化は起こっていないはずです。ただ、この Controller に関しては、authenticate_by_token
メソッドで自前のセキュリティゆるゆるのトークン認証を実装しているので、一旦は CSRF 保護をスキップします。それでは、動作を確認してみましょう。まずは作成です。ターミナルから
curl
コマンドを実行してサーバーに POST リクエストを送ってみます。api_token
はご自身のものに差し替えてください。先ほどログイン API で GET した API トークンです。% curl -X POST -d 'task[name]=by API&api_token=8YKPhsDNphD91j4EaQxyf6JF' localhost:3000/api/tasks実行したらブラウザーのタスク一覧画面か、一覧 API で返ってくる json を確認してみましょう。新しいタスクが追加されていれば ok です。
POST による作成が問題なければ、次は更新の PATCH リクエストを送ってみましょう。今作ったタスクを完了状態にしてみましょう。API トークンを差し替えるのは勿論のこと、URL の末尾の
Task
の ID も今作ったTask
の ID に差し替えてください。rails console
でTask.last.id
を叩けば最後に作ったTask
の ID が取れます。% curl -X PATCH -d 'task[finished]=true&api_token=8YKPhsDNphD91j4EaQxyf6JF' localhost:3000/api/tasks/7再びブラウザーのタスク一覧画面か、一覧 API で返ってくる json を確認してみましょう。完了状態になっている、あるいは
finished
がtrue
になっていれば ok です。ここまで実装できれば、モバイルアプリケーションからタスク管理するための準備は整いました。お疲れ様でした。
デプロイ
最後にこの web アプリケーションをどこかのサーバーに置く必要があります。本記事では無料かつクレジットカードの登録も必要がない Heroku にデプロイする手順を雑に書きますが、お好きなサーバーに上げてもらって大丈夫です。どこにも上げたくなくてお手軽にやりたければ、ngrok で
localhost:3000
をポートフォワーディングする手もあります。本記事では「web アプリケーションを作ったった!!」という気分になりたいので、とりあえず Heroku の無料枠にデプロイします。まずは Heroku にサインアップしてインストールガイドに従って heroku-cli をインストールします。Mac であれば現状は以下です。
% brew tap heroku/brew && brew install herokuHeroku にログインします。下記のコマンドを実行するとブラウザーからのログインが求められます。
% heroku loginHeroku では Git のリポジトリを push する形でデプロイするので、もしここまで Git 管理していなかった人は雑に commit してください。
% git commit -am "Create task management application"Heroku にアプリを作ります。このタイミングで URL が割り振られます。
% heroku create Creating app... done, ⬢ limitless-earth-31665 https://limitless-earth-31665.herokuapp.com/ | https://git.heroku.com/limitless-earth-31665.git
お好みで名前を変更してください。ドメインになります。勿論人と被ると弾かれます。
zone-web.herokuapp.com
は頂いた。% heroku rename zone-web Renaming limitless-earth-31665 to zone-web... done https://zone-web.herokuapp.com/ | https://git.heroku.com/zone-web.git
heroku create
した段階で、git remote
にheroku
が自動登録されているので、ここに push するだけでデプロイできます。3分ぐらい時間がかかると思うので、お茶でも飲みながら休憩しましょう。% git push heroku master無事にデプロイできたら、Heroku 上で
rails db:migrate
を実行してもらいます。heroku run
を頭に付けるだけでコマンドを走らせられるのは素晴らしいですね。% heroku run rails db:migrateこれで準備は完了です。
heroku create
かheroku rename
の時に割り振られた URL へアクセスしてみましょう。ログインページにリダイレクトされれば、ちゃんとアプリケーションが動いています。サインアップして自由にアプリケーションを操作してみましょう。これで、web アプリ編の演習は終了です。お疲れ様でした。モバイルアプリ編 でお会いしましょう。
終わりに
今回の演習では、最小限の認証付きタスク管理アプリケーションを開発し、モバイルアプリケーションに向けた API の実装を行いました。もし完走した方がいらっしゃいましたら、こっそり私(@OgiharaRyo)まで教えて頂けると嬉しいです。冒頭でも言い訳しましたが、Advent Calendar でやるには本当に本当に長い演習になってしまいました。重ねてお詫び申し上げます。
Ruby on Rails Advent Calendar 17日目は bake0937 さんです。
- 投稿日:2019-11-29T04:15:30+09:00
【Bootstrap】jQueryライブラリの読み込み【Rails Tutorial 8章まとめ】
BootstrapのjQueryライブラリ
Bootstrapに含まれるCSSのdropdownクラスやdropdown-menuといったドロップダウン機能は、Bootstrapのjavascript(jQuery)ライブラリを読み込まなければ使えない。
application.jsにBootstrapのJavaScriptライブラリを追加する。
app/assets/javascripts/application.js//= require rails-ujs //= require jquery //= require bootstrap //= require turbolinks //= require_tree .
- 投稿日:2019-11-29T04:02:47+09:00
#Rails ( #Ruby + ActiveSupport ) + Time.use_zone + travel_to で現地時刻=日本時刻=JSTを固定して UTC 変換結果を見て遊ぶ例
- タイムゾーンの9時間の扱いを一生かかっても覚えられる気がしない
- 足し算と引き算を間違える確率が65%
- Rubyで遊んでいたらだんだん分かってきた気がするけれど
- タイムゾーンに慣れ親しむだけで1週間かかって、3日で忘れる気がした
require 'active_support/core_ext' # 日本時間の年始の時間 jst_beggining_of_year = Time.use_zone('Tokyo') { Time.local(2020, 01, 01, 00, 00, 00) } # travel するために必要 require 'active_support/testing/time_helpers' include ActiveSupport::Testing::TimeHelpers # 日本時間の年始に時間を固定する travel_to jst_beggining_of_year # 今日の日付は 2020-01-01 Date.current # => Wed, 01 Jan 2020 # 現在時刻は 2020-01-01 00:00:00 で JST のタイムゾーンも持っている Time.now # => 2020-01-01 00:00:00 +0900 # こちらも同じく Time.current # => 2020-01-01 00:00:00 +0900 # Time.nowはRubyのclassだがちゃんとtravelできている Time.now.class # => Time # Time.currentはRails=ActiveSupportのclassみたいだがtravelできている # Time.zone= を指定していない場合は Ruby の Time.now と同じように働く気がしたが合ってるかな Time.current.class # => ActiveSupport::TimeWithZone # Time.zone.now メソッドでは、Time.zoneの指定をしていないので何も得られない # Time.currentとは扱いが違うみたいだ Time.zone.now # NoMethodError: undefined method `now' for nil:NilClass # Time.zone 指定を日本時間にする Time.zone = 'Tokyo' # => "Tokyo" # 現在時刻は 2020-01-01 00:00:00 で JST のタイムゾーンや曜日も持っている Time.zone.now # => Wed, 01 Jan 2020 00:00:00 JST +09:00 # Time.zone.now は ActiveSupport の class を持っている Time.zone.now.class # => ActiveSupport::TimeWithZone # 今日の日付は 2020-01-01だ # こちらも Ruby ではなく Rails = ActiveSupport のもの Date.current # => Wed, 01 Jan 2020 # Time.zone 指定を日本時間にする Time.zone = 'UTC' # => "UTC" # UTCでいうと今日の日付はまだ、前年の年末だ Date.current # => Tue, 31 Dec 2019 # 日本時間の年始の0時は、UTCでいうと9時間遅い、年末の15時だ Time.zone.now # => Tue, 31 Dec 2019 15:00:00 UTC +00:00Original by Github issue
- 投稿日:2019-11-29T03:15:24+09:00
#Ruby と #Rails の Time や Time.zone の扱いががマジで分からないし混沌としてるのでちょっとだけ整理したい。
Try at
この時間に試しました
- JST 2019-11-28 hour 19
- UTC 2019-11-28 hour 10
For Ruby
require ‘active_support/core_ext’
No Time zone setting
タイムゾーン指定がないとメソッドが使えなかったり
Time.zone # nil Time.zone.now # NoMethodError: undefined method `now’ for nil:NilClassTimezone指定
Time.zone は ActiveSupportのインスタンスになる
タイムゾーンを切り替えるにはTime.zone=
メソッドを実行Railsであればapplication.rb の configに書くだろうが
タイムゾーン指定自体は
Tokyo
でもAsia/Tokyo
でも同じようにはからってくれるっぽい
ちなみにTime.zone = ‘JST’
じゃ動かないとか信じられるかいTime.zone = 'UTC' => "UTC" Time.zone => #<ActiveSupport::TimeZone:0x00007f8610941ef0 @name="UTC", @tzinfo=#<TZInfo::DataTimezone: Etc/UTC>, @utc_offset=nil> => #<ActiveSupport::TimeZone:0x00007f86108d1a38 @name="Asia/Tokyo", @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>, @utc_offset=nil> Time.zone = 'Tokyo' => "Tokyo" Time.zone Time.zone = 'Asia/Tokyo' => "Asia/Tokyo" Time.zone => #<ActiveSupport::TimeZone:0x00007f86108d1a38 @name="Asia/Tokyo", @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>, @utc_offset=nil> Time.zone = 'JST' # ArgumentError: Invalid Timezone: JSTUTC
UTCをtimezone指定した場合
Time.zone = ‘UTC’ # => “UTC” Time.zone # => #<ActiveSupport::TimeZone:0x00007fe40e15ed08 @name=“UTC”, @tzinfo=#<TZInfo::DataTimezone: Etc/UTC>, @utc_offset=nil> Time.zone.now # => Thu, 28 Nov 2019 10:00:31 UTC +00:00 Time.zone.parse(‘2020-01-01 00:00:00’) # => Wed, 01 Jan 2020 00:00:00 UTC +00:00JST
JSTをtimezone指定した場合
Time.zone = ‘Tokyo’ # => “Tokyo” Time.zone # => #<ActiveSupport::TimeZone:0x00007fe40d2721a0 @name=“Tokyo”, @tzinfo=#<TZInfo::DataTimezone: Asia/Tokyo>, @utc_offset=nil> Time.zone.now # => Thu, 28 Nov 2019 19:00:47 JST +09:00 Time.zone.parse(‘2020-01-01 00:00:00’) # => Wed, 01 Jan 2020 00:00:00 JST +09:00Time系
こちらはRuby系なので
Time.zone
を参照しない、というよりTime.zone
なんていうメソッドもクラスもない
Rails = ActiveSupport の Time.zone 系メソッドとは分けて考えることRuby で最初から出来るかと思いきや、 一部のメソッドは requireが必要という罠 -> Time.parse とか
require active support でも動くようになるので、まさかのactivesupportのメソッドかと思って驚いたが違ったタイムゾーンを変えるには ruby実行時の環境変数を変える
TZ=UTC ruby ... TZ=Asia/Tokyo ruby ...環境変数指定もしくはシステムのタイムゾーンによって結果が変わる
Rubyだけの処理でもタイムゾーン情報を持っているようだTime.now # => 2019-11-28 10:19:37 +0000 Time.now # => 2019-11-28 19:19:43 +0900 # Time.parse / Time.zone.parse Time.parse(‘2020-01-01 00:00:00’) # NoMethodError: undefined method `parse’ for Time:Class Time.parse(‘2020-01-01 00:00:00’) # => 2020-01-01 00:00:00 +0900 Time.zone = ‘UTC’ # => “UTC” Time.zone.parse(‘2020-01-01 00:00:00’) # => Wed, 01 Jan 2020 00:00:00 UTC +00:00RubyっぽいのにRails? current系
nowじゃなくてcurrentっていう名前のメソッドはRubyじゃなくてRails/AcviveSupportのものだ
Time.current # NoMethodError: undefined method `current’ for Time:Class require ‘active_support/core_ext’ Time.current # => 2019-11-28 19:21:56 +0900 なのにclassはTimeという奇特なやつ Time.current.class # => Timeちなみに
Date.today
は Ruby のものだがDate.current
Date.yesterday
Date.tomorrow
は Railsのものだという大いなる罠もあるOriginal by Github issue
- 投稿日:2019-11-29T02:58:55+09:00
Railsでのjoinsの結果のidについて
Railsでテーブル結合してデータを取得した時、idを参照元の主キーにしたいのに参照先の主キーになってしまっていた。
結論としてHoge.joins(:fuga).where(fuga_name:"hoge").select("fuga.*,hoge.*")このようにselectで最後に指定したテーブルの主キーが結合したデータの主キーとなる。