テキスト内のURLがaタグに変換されるようにする

目的

タスク管理アプリのタスクの説明部分にリンクを追加できるようにしたい。

つまり、text_areaなどで入力したテキストを表示する際にURLを含んでいたら自動的にリンクを作るようにしたい、ということです。

helperメソッドを作成

URIライブラリを使います。

module ApplicationHelper
require 'uri'  
  def convert_url_into_a_tag(text)
    URI.extract(text, ['http', 'https']).uniq.each do |url|
      sub_text = ""
      sub_text << "<a href=" << url << " target=\"_blank\">" << url << "</a>"

      text.gsub!(url, sub_text)
    end
   return text
  end
end

詳しい解説

URI.extract(text, ['http', 'https']).uniq.each do |url|

第一引数に与えられたテキスト(text)のなかから第二引数に与えられたスキーム名(httpとhttps)にマッチするものを探し出し、.uniqで重複を削除してurlとしてブロックが受け取ります。

 sub_text = ""
 sub_text << "<a href=" << url << " target=\"_blank\">" << url << "</a>"

 text.gsub!(url, sub_text)

ブロック内の処理は、用意した空のローカル変数sub_textにそれぞれの文字列を追加していきtext.gsub!(url, sub_text)でtext内のurlに該当する部分をsub_textに置き換えます。

注意
レシーバ.gsub(引数) → 置き換えを行った新しい文字列を返す
レシーバ.gsub!(引数) → レシーバ自身を置き換えて返す

最終的に、return textで「URLをaタグに置き換えたテキスト(text)」を返します。

使い方

以下のように記述して使います。

 <%= convert_url_into_a_tag(h(@task.description)).html_safe %>

h と html_safe

(h(@task.description)).html_safe

hというのはhtml_escapeメソッドの略名で、HTMLタグなどをエスケープするメソッドです。

.html_safeというのは、「レシーバに含まれる文字列は問題ないのでエスケープしないでね」というメソッドです。

2つは反対の意味を持つメソッドのようですが、なぜ一緒に使われているのでしょうか……

セキュリティ対策としてとても重要

まず、tasksコントローラから渡された@taskのdescription(タスクの説明文)から、html_escapeメソッドによってHTMLタグがエスケープされます。
これはユーザーが悪意のあるスクリプトを埋め込む事を防止する意味合いがあります。(クロスサイトスクリプティングXSS

そうしてXSSの対策をしたあとで、convert_url_into_a_tagメソッドによってテキスト内のURLをaタグに置き換えます。
そのときにつけたHTMLタグは安全ですので、html_safeメソッドで有効化する、という流れです。

ユーザーによる入力をエスケープした上で、改めてこちらでaタグを付与する ということですね。

参考リンク

[Ruby][Rails]テキスト内のURLをaタグに書き換える

[Rails]ERBのエスケープを自在に扱おうぜ

「オブジェクト指向でなぜつくるのか」を読んだ

オブジェクト指向に対する誤解が解けた

プログラミング言語は、機械語によるプログラミングからアセンブリ言語高級言語→構造化言語というように、「より便利に」を目指してそれぞれの課題を解決するように進化してきました。
なかでも「無駄を省く」「保守と再利用をしやすいようにする」という課題の解決に向かって生まれたのがオブジェクト指向プログラミングです。
あくまでもプログラミング技術の1つの到達点であり、現在もさらに便利になるように新しい理論や言語が開発され続けています。


オブジェクト指向は2つの側面を持つ

オブジェクト指向プログラミングの考え方は図式表現、モデリング、設計などの上流工程の分野にも応用されました。
オブジェクト指向」という言葉は下流工程の「プログラミング技術」と上流工程の「汎用の整理術」の2つの側面を持っています。
この2つを混同してしまうことがオブジェクト指向の理解を難しいものにしており、この本は2つを明確に区別して書かれています。


それぞれの技術・手法について

クラス、ポリモーフィズム、継承などの要素について、第五章で静的領域、ヒープ領域、スタック領域からなるプログラムのメモリ領域がOOPではどのように管理/活用されているかという内部の動きを解説することで、それぞれの要素を抽象的な概念ではなくシステムとしての働きとして理解できるようになっています。

上流工程についても、業務分析・要求定義・設計の一般的な説明をしたうえで、実際にUMLを活用して図書館の貸出業務をモデリングするプロセスを3ステップに分けて解説してくれるので、ついていくことができました。


感想

初学者の自分はオブジェクト指向とは「なにか全く新しい完成した万能の技術」だと思っていましたが、その誤解が解けたのがいちばんの収穫でした。
以前のプログラミング技術ではどんな課題があってオブジェクト指向がそれをどう解決したか、という視点でオブジェクト指向プログラミングを解説している点が分かりやすかったです。
パソコンの中で動くプログラムだけでなく、実際の開発業務の流れさえもそれぞれをリソースとして捉え、より効率的に行うことを目指して世界中でノウハウが共有されていることに驚きました。プログラマーには「無駄を省いて効率的に」を追求し続ける精神が必要なんだと感じました。

ネストしたリソースのユーザー制御

完成したタスク管理アプリに、「ひとつのタスク内にサブタスクを作成できる機能」をアップデートで追加しました。

Subtaskモデルを作成したので、現在ルーティングはこうなっています。

Rails.application.routes.draw do
 
  get 'static_pages/home'
  root to: "static_pages#home"
  devise_for :users

  resources :users, only: [:index, :show] do  
    resources :tasks do
      put :sort
        resources :subtasks, only: [:create, :new, :show, :update, :destroy]  do
          put :sort
        end
    end
  end

end

コントローラーのアクションもひと通り実装し、機能テストも作成しました。
ユーザー制御がややこしかったので、失敗するテストを先に書いてTDDで進めました。
例えばsubtasks#updateのURLは

/users/:user_id/tasks/:task_id/subtasks/:id

というふうに ID を3つ使います。
サブタスクモデルはtask_idでタスクモデルと紐付き、タスクモデルはuser_idでユーザーモデルと紐付いています。
user_idのみでユーザー制御をするとURLのtask_idを書き換えた場合他人の情報にアクセスできてしまうという問題がありました。

そして、考えに考えた結果実装した before_actionのメソッドが以下です。

    #正しいユーザーかどうか確認する
    def correct_user
       if params[:id].present?
         @subtask = Subtask.find(params[:id])
         @task = Task.find(@subtask.task_id)
         redirect_to(root_path, notice: "URLが間違っています") unless @task.user_id == current_user.id
       else
         @task = Task.find(params[:task_id])
         @user = User.find(@task.user_id)
         redirect_to(root_path, notice: "URLが間違っています") unless @user == current_user
       end
     end

長いし、モデルを追加するたびにこの処理も追加しなくてはいけないのかと思うと先が思いやられます。
モデル側のバリデーションという形でユーザー制御を実施できるというような情報をチラッと見たので調べてみます。

テストファイル内の個々のテストやsetup、コントローラーのprivateアクションなどが複雑になってきました。
リファクタリングの必要がありそうです。


ちなみにPATCHリクエストを送る際、パラメータのtask_idを他人のIDに変更して送信できてしまうと結果として「他人のサブタスクを作成できてしまう」
状態になるのですが、それはストロングパラメーターの仕組みによって制御されています。

      def subtask_params
        params.require(:subtask).permit( :title, :description, :row_order_position)
      end  

Herokuへのデプロイ時に苦労した話

昨日完成したTodoアプリをHerokuにアップしました!

その際2箇所でつまづいたので、記録しておきます。

pg gemがインストールできない

HerokuではSQLiteがサポートされていないのでPostgreSQLを使います。

sqlite3 gemをproduction環境で使わないように、
そしてpg gemをproduction環境で使うようにgemfileを編集し、bundle installしたところ

Building native extensions.  This could take a while...
ERROR:  Error installing pg:
    ERROR: Failed to build gem native extension.
(略)
Can't find the 'libpq-fe.h header

libpq-fe.h が見つからない、というエラーが出ました。

このエラーの処理は

$ sudo yum install postgresql-devel

で解決。
(yumというのを使ってPostgreSQLをインストールするということらしいです)


(本筋と関係なし)

無事にpg gemもインストールできたのでherokuにpushしました。
デプロイしたページにアクセスすると……




We're sorry, but something went wrong

ああ……

色々調べた結果、

「production環境でrails_12factorというgemを有効にする必要がある」

という情報にたどり着き、gemfileに追記したのですが
調べ直したところこの情報はRailsチュートリアル第4版から、つまりRails5になってからは記載が無くなった情報なので、今はもう必要ないのかもしれません。

本題は次のエラーです。

Userテーブル作成時のマイグレーションファイルを削除してしまった

heroku logsで確認してみると、rootページへのGETリクエストは成功しているけどその後移動するログインページで500エラーが出ていました。

データベースの問題じゃないかと思ってdb:migrateを実行してみると……

$ heroku run rake db:migrate
Running rake db:migrate on ⬢ rocky-beyond-45744... up, run.9472 (Free)
   (0.8ms)  SELECT pg_try_advisory_lock(2653661978130648765)
   (1.3ms)  SELECT "schema_migrations"."version" FROM "schema_migrations" ORDER BY "schema_migrations"."version" ASC
Migrating to AddUserRefToTasks (20180827040204)
   (0.8ms)  BEGIN
== 20180827040204 AddUserRefToTasks: migrating ================================
-- add_reference(:tasks, :user, {:foreign_key=>true})

  (略)

   (1.4ms)  ROLLBACK
   (5.0ms)  SELECT pg_advisory_unlock(2653661978130648765)
rake aborted!
StandardError: An error has occurred, this and all later migrations canceled:

PG::UndefinedTable: ERROR:  relation "users" does not exist

(略)

Caused by:
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  relation "users" does not exist

usersテーブルが存在しない、というエラーが出ました。

ん?と思ってdb/migrateを確認すると、Userモデルを作成したときのマイグレーションファイルが無い。

Devise導入の際にrails g devise UserでUserモデルが作成できているのかわからず試行錯誤したのですが、
どうやらその時に作成されたマイグレーションファイルを削除してしまったようです。

解決方法

joppot.info

この記事を参考に以下の処理をしました。

状況

削除してしまったマイグレーションファイルのIDを調べます

$ bundle exec rake db:migrate:status
database: 省略/db/development.sqlite3

 Status   Migration ID    Migration Name
--------------------------------------------------
   up      20180823030402  Create tasks
   up     ★20180826083613  ********** NO FILE **********
   up      20180827040204  Add user ref to tasks
   up      20180829041636  Add row order to tasks

NO FILE になっているIDが今回間違えて削除してしまったマイグレーションファイルです。

そして、Statusがupになっているマイグレーションファイルはデータベースに取り入れられているものです。

本番環境でマイグレーションを実行した時に、★20180826083613のファイルが存在しないので冒頭のエラーが出てしまった、という状況ですね。

対処方法

プロジェクトのdb/migrateディレクトリに移動します

$ cd db/migrate

ここで、先程確認した間違えて消してしまったファイルと同じIDのマイグレーションファイルを作成します。

$ touch マイグレーションID_ファイル名.rb

今回の場合だと

$ touch 20180826083613_devise_create_users.rb

になります。

作成したマイグレーションファイルに内容を書き込みます。
私の場合はDevise練習用に作成した別のプロジェクトがあったのでそこから引っ張ってきました。

# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""
 
(略)

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

これで間違えて削除してしまったマイグレーションファイルが復活した状態になったので

$ git add .
$ git commit
$ git push heroku master

でherokuにプッシュし、再度 本番環境でdb:migrate

$ heroku run rake db:migrate

うまくいきました!
これでアプリケーションの作成からHerokuへのデプロイまでをひと通り完了することができました。
次に何をするか考えようと思います。

参考リンク

Herokuへのデプロイでつまづいたことまとめ

libpq-fe.h が見つけられない。

railsのrakeのmigrationファイルを削除しNO FILEとstatusに出た時の対処 – joppot

jQueryUIのsortableで並び替えが保存されない

Rails5.2.1

初の自作アプリとしてタスク管理アプリケーションを作成すべくこの一週間奮闘してきまして、本日とりあえずの完成に至りました。

Taskの並び替えをドラッグ&ドロップで実現すべくjQueryUIの導入を目指してドン詰まったのが前回の記事でしたが、その後並び替えたTaskの順番が保存されないという問題が新たに浮上しまして、その解決までの道のりを記しておきます。

エラー内容

Started PUT "/users/1/tasks/51/sort" for 157.107.100.245 at 2018-08-30 10:33:07 +0000
Cannot render console from 157.107.100.245! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255 
Processing by TasksController#sort as JSON
  Parameters: {"task"=>{"row_order_position"=>"1"}, "user_id"=>"1", "task_id"=>"51"}
★Can't verify CSRF token authenticity.
Completed 422 Unprocessable Entity in 0ms (ActiveRecord: 0.0ms)

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):
vendor/bundle/ruby/2.4.0/gems/actionpack-5.2.1/lib/action_controller/metal/request_forgery_protection.rb:211:in `handle_unverified_request'
vendor/bundle/ruby/2.4.0/gems/actionpack-5.2.1/lib/action_controller/metal/request_forgery_protection.rb:243:in `handle_unverified_request'
vendor/bundle/ruby/2.4.0/gems/devise-4.5.0/lib/devise/controllers/helpers.rb:255:in `handle_unverified_request'(こんな感じのがたくさん

エラー部分抜粋

Can't verify CSRF token authenticity.
Completed 422 Unprocessable Entity in 0ms (ActiveRecord: 0.0ms)

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken):

これに関してはこちらの記事がとても参考になりました。

Rails4のCSRF対策で「Can't verify CSRF token authenticity」エラー

RailsCSRF対策によって発生しているエラーですが、動き自体は正常なものなので、

class TasksController < ApplicationController
 protect_from_forgery except: :sort
end

と記述することでsortアクションを例外に指定します。


これで解決したと思いきや、次のエラーが

Started PUT "/users/1/tasks/52/sort" for 157.107.100.245 at 2018-08-30 11:16:00 +0000
略
 [1m[35m (5.4ms)[0m  [1m[36mcommit transaction[0m
  ↳ app/controllers/tasks_controller.rb:60
Completed 500 Internal Server Error in 32ms (ActiveRecord: 7.6ms)
  
ActionView::MissingTemplate (Missing template tasks/sort, application/sort with {:locale=>[:en], :formats=>[:json, :js, :html, :text, :js, :css, :ics, :csv, :vcf, :vtt, :png, :jpeg, :gif, :bmp, :tiff, :svg, :mpeg, :mp3, :ogg, :m4a, :webm, :mp4, :otf, :ttf, :woff, :woff2, :xml, :rss, :atom, :yaml, :multipart_form, :url_encoded_form, :json, :pdf, :zip, :gzip], :variants=>[], :handlers=>[:raw, :erb, :html, :builder, :ruby, :slim, :coffee, :jbuilder]}. Searched in:
  * "/home/ec2-user/environment/TodoApp3/app/views"
  * "/home/ec2-user/environment/TodoApp3/vendor/bundle/ruby/2.4.0/gems/devise-4.5.0/app/views"
):
  
app/controllers/tasks_controller.rb:61:in `sort'

ActionView::MissingTemplate (Missing template tasks/sort, application/sort with
という部分から「tasksコントローラのsortアクションで描写するviewがないよ!」
という意味だと読み取れます。
最後の行にあるapp/controllers/tasks_controller.rb:61:in `sort'に従って61行目を確認すると……

  def sort
    @user = User.find(params[:user_id])    
    task = Task.find(params[:task_id])
    task.update(task_params)
⇛ render nothing: true
  end

ここが良くないようです。

調べてみるとどうやら、

render nothing: true はRails5から使えなくなったので変わりに

render body: nil あるいは head :ok を使う

ということだったようです。

感想

Rails4を想定して書かれた記事を参考にしていたばかりに、思わぬところで時間を取られてしまいました。
やり方を調べればいくらでも記事は出てきますが、情報が古い可能性があるということ、プログラミングで使うものは日々アップデートされるということをしっかり理解していないと今回のような事態になってしまいます。なんでもコピペではダメですね。

application.jsの記述が原因でjQueryUIが動かなかった話

前置き

作成中のTodoアプリケーションで、タスクのドラッグ&ドロップで並び順を自由に替えられるようにしよう!ということで

qiita.com

こちらの記事を参考に実装を進めていたんですが、手順を完了してもドラッグ&ドロップが動かない……
application.jsファイルへの記述が足らなかったことが原因でした。

今回はこのapplication.jsファイルの役割とjQueryを動かすのに必要な記述をまとめていきます。

前提

そもそもこのapplication.js、ひいてはassetsディレクトリって一体何なの?ということなんですが、
アセットパイプライン(Asset Pipeline)というRailsの機能の一部分であります。

アセットパイプラインについてはRailsチュートリアル第4版の5.2.1、 Rails初学者がつまずきやすい「アセットパイプライン」が分かりやすいです。

要するに
CSSJavaScript、画像など、HTMLを装飾する要素を専用のディレクトリにまとめることで開発をしやすくし

②実際にアプリケーションが動作するときにはそれらのファイルがアセットパイプラインの機能によって効率的にまとまり、ファイルサイズを最小化してくれるのでサイトの読み込み時間も短くなる
という便利な仕組みです

application.js って何?

アセットパイプラインではCSSJavaScript、画像などの「アセット」はapp/assetsディレクトリ配下にあるそれぞれのフォルダに置きます。
f:id:naito-coding0322:20180830165306p:plain

JavaScriptsとstylesheetsフォルダにはそれぞれapplication.js / application.scssというファイルがあります。

このファイルの役割は
このRailsアプリケーションでは、このJavaScript/CSSを使うよ~
という宣言をすることです。

この宣言(ディレクティブ)によってアセットパイプラインのSprocketsという機能がファイルを読み込んで効率的にまとめてくれます。

application.jsの中身

今回はこのapplication.jsファイルの記述を間違えていたのでjQueryUIを利用した並び替えがうまく動きませんでした。
かなり長い時間を取られてしまったので、このファイルの中身についてまとめておきます。 (説明を簡略化するため、jQueryUIの動作に関するもののみ記述しています)

//
//= require rails-ujs
//= require jquery
//= require turbolinks
//= require jquery-ui/widgets/sortable
//= require_tree .

最終的にこんな感じできちんと動作しました。一つ一つの記述を解説していきます。

(//= はSprocketsに対してどのJavaScriptを使うかという宣言のやり方です )


//= require rails-ujs

RailsJavaScriptを使えるようにする
//= require jquery_ujsという記述はrails5.1から不要になったようです


//= require jquery

jQueryを読み込む


//= require turbolinks

ページ遷移をAjaxに置き換え、高速化する。Rails4から標準装備になった。


//= require jquery-ui/widgets/sortable

jQueryUIのsortable機能を使えるようにする


//= require_tree .

app/assets/javascripts以下の全てのjsファイルを読み込む
(これによってtable_sort.js.coffeeが読み込まれる)

注意点

実装したjQueryの機能がページ遷移後に動かなくなる場合、Rails5以降は以下の記事にあるような処理が必要です!

qiita.com

Deviseインストール直後のテストで失敗

自作のアプリにログイン機能を実装しようということで Devise を試してみました。

qiita.com

こちらの記事を参考にさせていただいたんですが、ひと通り設定が終わってrailsサーバー再起動のあとログイン画面を確認できたので、自動で作成されたテストを試しに実行してみたら以下のエラーが

Error:
HomeControllerTest#test_should_get_index:
ActiveRecord::RecordNotUnique: SQLite3::ConstraintException: column email is not unique: INSERT INTO "users" ("created_at", "updated_at", "id") VALUES ('2018-08-26 07:01:20.987535', '2018-08-26 07:01:20.987535', 298486374)


いろいろ検索してスタックオーバーフローを見た結果、自動生成されたfixtureのymlファイルに問題がありました。

one: {}
# column: value
#
two: {}
# column: value

自動生成されるテストユーザーが2人いるんですが、それが同じ内容なのでemailが被ってしまっていたんですね。

one:
  email: user@example.com
  encrypted_password: password1

two:
  email: user2@example.com
  encrypted_password: password2

上記のように書き換えて解決。
テストも成功しました。

<参考にしたページ>

ruby on rails - SQLite3::ConstraintException: column email is not unique - Stack Overflow