Naomi's notebook

Naomi's notebook

Ruby on Rails tutorial +alpha(6)デプロイ

注意:この記事はメモなので、この記事に書いてあることをやっただけでは同じものができません。(大体の雰囲気は書いてあると思います。)

注意:全くのweb開発初心者なので、間違っているところなどがあるかと思いますが、教えていただけると嬉しいです。


とりあえず最低限の機能っぽいものを実装したので、herokuでデプロイしてみようと思います。 さくらのVPS上でデプロイしてみようとしたのですが、fullcalendar.ioの表示がうまくいかなかった…

naomi-notebook.hatenablog.com

heroku create
heroku maintenance:on
git push heroku
heroku rename naomiatlibrary-life-habits
heroku pg:reset DATABASE
heroku run rails db:migrate
 heroku run rails db:seed
heroku maintenance:off

あっ忘れてた、puma.rbから以下のサーバー用の記述を抜かないといけません。

if "production" == ENV.fetch("RAILS_ENV") { "production" }
​    cert = "/etc/letsencrypt/live/[domain]/fullchain.pem"
​    key = "/etc/letsencrypt/live/[domain]/privkey.pem"
​    ssl_bind "0.0.0.0", 9292, cert: cert, key: key

end

あとは、turorialだとsendgridを使っていますが、heroku経由だと結構な高確率でいきなりbanされるらしいので(されてしまいました…)、サーバにデプロイした時と同じようにgmailのアプリパスワードを使いました。

なんでやねん! f:id:Naomi_Lilienthal:20200817204007p:plain

どうやらconfig/webpacker.ymlのextractcss:trueをfalseにしなければいけなかったようです。もしかしてこれでサーバの方もいける…? 3rd party css not compiled in production on Heroku (Rails 6 - Webpack ) - Stack Overflow

できた!f:id:Naomi_Lilienthal:20200817204820p:plain

Ruby on Rails tutorial +alpha(5)マイクロポストもカレンダー上に表示&マウスオンでポスト表示

注意:この記事はメモなので、この記事に書いてあることをやっただけでは同じものができません。(大体の雰囲気は書いてあると思います。)

注意:全くのweb開発初心者なので、間違っているところなどがあるかと思いますが、教えていただけると嬉しいです。

naomi-notebook.hatenablog.com f:id:Naomi_Lilienthal:20200817135902p:plain だいぶ出来てきたので、今まで作った機能をおさらいして、次何をやるか考えます。考えて実装したら続きを書きます。

機能として必ず必要なもの

  • 当たり前ですが、eventを作成する画面を作る(とりあえずフォームで、それができたらもっと優れたやりやすい方式で)
  • eventを後から変更する画面を作る

使いやすさの面で必要そうなもの

  • eventをタイマーなどでリアルタイムに作成できるようにする(studyplusアプリのアレみたいなイメージです)
  • カレンダーをドラッグするなどしてもっと簡単に記録が作れるようにする
  • カレンダー上の予定をクリックすると編集画面が出てくる
  • eventtype>4とタイトルの組み合わせを自分で設定できるようにする
  • eventtype=1の睡眠データを集めてグラフかなんかに表示できるようにする

生活記録表として必要な要素

  • 睡眠とベットに入っているだけを分ける
  • all-dayに気分の記録ができるようにする→type=0の時jsonにallday属性を作ることで解決しました。

個人情報の扱い的に正しそうなもの

  • ログインした時やフォローされている時しかevents/(user_id)/eventsでjsonデータが返されないようにした方が個人情報的には良さそうです
  • 上に付随して、フォローされるのを許可制にできるようにする
  • マイクロポストもフォロワーにしか見られないようにする
  • 自分でアカウントが消せるようにする

スケーリングのために必要そうなこと

  • fullcalendarが/users/(user_id)/eventsにアクセスするときに全てのuser.eventsを取得するのではなく、endtimeがstartより後、またはstarttimeがendより前のもののみ取得する(whereとかを使えそう)

その他

  • micropostをチュートリアルで作っちゃったのでその有効な活用方法を考える笑→メモを残した時間がカレンダー上に表示され、それをクリックとかするとマイクロポストが表示される、とか…?

micropostが持て余し気味なのでmicropostを改造していこうと思います。

git checkout renew_micropost

まず、自分でないかつフォローしてないとmicropostをshowで表示しないようにします。(viewを変えればすぐできます。)

次に、自分またはフォローしている時にmicropost一つ一つの内容を返すページを作ります。micropostの内容をクリックするとmicropostのページに飛ぶようにしてみます。

def show
      @micropost = Micropost.find(params[:id])
      if current_user && ( @micropost.user_id == current_user.id || current_user.following.include?(@micropost.user_id ) )
        #フォローしていれば返す
        @micropost
      else
        redirect_to root_url
      end
    end

routes.rb

resources :microposts,          only: [:create, :destroy, :show]

次に、eventsと一緒にjson出力するようにします。

json.array!(@microposts) do |micropost|
    json.id micropost.id
    json.micropost true
    json.title "ポスト"
    json.start micropost.created_at
    json.end micropost.created_at
    json.color "lightblue" 
end

次に、micropost==trueの時はクリックしてもevent編集画面には飛ばないで、micropostの画面に飛ぶようにします

eventClick: function(info) {
if(info.event.extendedProps.micropost){window.location.replace("/microposts/"+info.event._def.publicId);}
        else{window.location.replace("/events/"+info.event._def.publicId+"/edit/");}
      } 

次に、これをちょっと変えて画面内に表示するようにします。 jQueryのAjaxを使って外部HTMLの一部を抜き出し挿入する - Qiita

javascript - Jquery filter not working with jquery object - Stack Overflow

これらを参考にしました。(filterは子要素を見つけられないみたいだった…)

eventMouseEnter: function(info) {
        if(!info.event.extendedProps.micropost){return;}
        url="/microposts/"+info.event._def.publicId;
        $.get(url,{},
          function(data){
            var html= $($.parseHTML( data ));
            mnumber="#micropost-"+info.event._def.publicId
            $("#box_load").empty();
            $("#box_load").append(html.find(mnumber)[0]);
        });
      },
      eventMouseLeave: function(info) {
        if(!info.event.extendedProps.micropost){return;}
        $("#box_load").empty();
      }
#box_load {
  width: 100%;
  height: auto;
  position: absolute;
  z-index: 9999;
  box-sizing: border-box;
  word-wrap: break-word;
    top: 0;
    left: 0;
    overflow-y: auto;
  overflow-x: auto;
  background: white;
}

これでカーソルがポストの上にある時にポストをカレンダーに重ねて表示することができるようになりました。(本当はもっとかっこよくホバーしてくるように作りたいのですが、cssが難しい…)

f:id:Naomi_Lilienthal:20200817172844p:plain
マウスカーソルがカレンダー上のポストの上にある時の様子

Ruby on Rails tutorial +alpha(4)フォローを承認制にする

注意:この記事はメモなので、この記事に書いてあることをやっただけでは同じものができません。(大体の雰囲気は書いてあると思います。)

注意:全くのweb開発初心者なので、間違っているところなどがあるかと思いますが、教えていただけると嬉しいです。

naomi-notebook.hatenablog.com

フォローを承認制にしていきます。

フォロー関係を作った時と同様に FollowRequestモデルを作り、以下の機構を実装していきます。

  • follow_requestsテーブルにfollower_idとfollowed_idを追加、usersと紐づける
  • follower_idの人がフォローリクエスト申請ボタンを押したら テーブルにデータ作成 *followed_idの人がフォロー許可を押したらrelationshipsテーブルにデータを作成してfollow_requestsのデータは削除

以下の動作が保証されなければいけないでしょう。

  • follow_requestsをみられるのは関係する2人のみ
  • follow_requestsを送信できるのはfollower_idだけ
  • follow_requestsを承認できるのはfollowed_idだけ
  • 存在しないフォローリクエストは承認できない

モデルの作成

モデルを作ります。

rails generate model FollowRequest follower_id:integer followed_id:integer

migration

class CreateFollowRequests < ActiveRecord::Migration[6.0]
  def change
    create_table :follow_requests do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :follow_requests, :follower_id
    add_index :follow_requests, :followed_id
    add_index :follow_requests, [:follower_id, :followed_id], unique: true
  end
end

model/follow_request.rb

class FollowRequest < ApplicationRecord
    belongs_to :follower, class_name: "User"
    belongs_to :followed, class_name: "User"
    validates :follower_id, presence: true
    validates :followed_id, presence: true
end

model/user.rb

has_many :active_requests, class_name:  "FollowRequest",
                                  foreign_key: "follower_id",
                                  dependent:   :destroy
  has_many :passive_requests, class_name:  "FollowRequest",
                                  foreign_key: "followed_id",
                                  dependent:   :destroy
  has_many :request_following, through: :active_requests, source: :followed
  has_many :request_followers, through: :passive_requests, source: :follower
rails db:migrate:reset

テストを書いてフォローリクエストが送れるようにします。 test/models/follow_request_test.rb

test "フォローリクエストを送る" do
    michael = users(:michael)
    archer  = users(:archer)
    assert_not michael.request_following?(archer)
    michael.request_follow(archer)
    assert michael.request_following?(archer)
    assert archer.request_followers.include?(michael)
    michael.request_unfollow(archer)
    assert_not michael.request_following?(archer)
  end

models/user.rb

# ユーザーをフォロー申請する
  def request_follow(other_user)
    request_following << other_user
  end

  # ユーザーをフォロー申請解除する
  def request_unfollow(other_user)
    active_requests.find_by(followed_id: other_user.id).destroy
  end

  # 現在のユーザーがフォローしてたらtrueを返す
  def request_following?(other_user)
    request_following.include?(other_user)
  end

まずフォローリクエストの数をホームに表示することにして、そこをクリックするとフォローリクエストが見られるようにします。 routes.rb

resources :users do
    member do
      get :following, :followers
      get :request_following, :request_followers
      get :events
    end
  end

だいたい同じように作ります。

コントローラとビューの作成

ここまではRelationshipsを作る時と同じでした。ここから、フォローボタンを押すとフォローリクエストが送られるようにします。 (フォローの時のコードと同じようにやる)

次に、ユーザの下のボタンでフォローを承認できるようにします

class RelationshipsController < ApplicationController
    before_action :logged_in_user
  
    def create
      @user = User.find(params[:follower_id])
      if @user.request_unfollow(current_user)
        @user.follow(current_user)
        respond_to do |format|
          format.js {render inline: "location.reload();" }
        end
      else
        flash[:danger]="エラーが発生しました。フォローリクエストが存在しません。" 
        redirect_to :back
      end
    end
  
    def destroy
      @user = Relationship.find(params[:id]).followed
      current_user.unfollow(@user)
      respond_to do |format|
        format.html { redirect_to @user }
        format.js
      end
    end
  end
<li>
  <%= image_tag user.icon_image(size:50), :size=>"50x50"%>
  <%= link_to user.name, user %>
  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, method: :delete,
                                  data: { confirm: "You sure?" } %>
  <% end %>
  <% if current_user.request_follower?(user) %>
    <%= form_with(model: current_user.active_relationships.build, remote: true) do |f| %>
      <div><%= hidden_field_tag :follower_id, user.id %></div>
      <%= f.submit "フォロー承認", class: "btn btn-primary" %>
    <% end %>
  <%end%>
</li>

できました。 f:id:Naomi_Lilienthal:20200816234856p:plainf:id:Naomi_Lilienthal:20200816234912p:plain

権限設定・テストなど

あとはテストなどを書いていきます。createを使って直接フォローができないようになっているか注意しましょう。

自分以外は/users/1/request_followersと/users/1/request_folloingを見られない

before_actionを指定すれば良いです。画面上に存在するボタンを押したらルートに飛ばさせるのも変なので、ビューの方でcurrent_user=@user以外の時はリンクを消しておきました。

フォロー関係のテスト

folloing_testを変更すればいいです。

  test "should send and accept follow request a user the standard way" do
    assert_difference '@user.request_following.count', 1 do
      post follow_requests_path, params: { followed_id: @other.id }
    end
    log_in_as(@other)
    assert_difference '@user.following.count', 1 do
      post relationships_path, params: { follower_id: @user.id }
    end
  end

  test "should accept follow request a user with Ajax" do
    assert_difference '@user.request_following.count', 1 do
      post follow_requests_path, xhr: true, params: { followed_id: @other.id }
    end
    log_in_as(@other)
    assert_difference '@user.following.count', 1 do
      post relationships_path,xhr:true, params: { follower_id: @user.id }
    end
  end

  test "should not accept not existing follow request" do
    log_in_as(@other)
    assert_no_difference '@user.following.count' do
      post relationships_path, params: { follower_id: @user.id }
    end
  end

これだとajaxではなくフォローを承認したときにテストが失敗してしまいます。これはformat.htmlの方を返していなかったからです。

def create
      @user = User.find(params[:follower_id])
      if @user.request_unfollow(current_user)
        @user.follow(current_user)
        respond_to do |format|
          format.html {redirect_back(fallback_location: root_path) }
          format.js {render inline: "location.reload();" }
        end
      else
        flash[:danger]="エラーが発生しました。フォローリクエストが存在しません。" 
        redirect_back(fallback_location: root_path)
      end
    end

にしたら通りました。(あとrails5以上ではredirect_to :backが消えていたみたいです。redirect_back(fallback_location: root_path)にしました。) あとは細かい訂正をすれば(省略)テストに全て通過します。

Railsをproduction環境で動かす時のメモ

注意:この記事はメモなので、この記事に書いてあることをやっただけでは同じものができません。(大体の雰囲気は書いてあると思います。)

注意:全くのweb開発初心者なので、間違っているところなどがあるかと思いますが、教えていただけると嬉しいです。


基本的にはググって出てくる方法と同じようにやっていますが、書いてなかったことを順次追加していきます

ssl接続ができるようになるまでに必要なこと

SSL証明書を発行する

ネコでもわかる!さくらのVPS講座 ~第六回「無料SSL証明書 Let’s Encryptを導入しよう」 | さくらのナレッジ

サーバをSSL化する Let's Encryptを利用してApache 2.4サーバをHTTPS化する - Qiita

railsSSL証明書を登録する

Rails,HTTP parse error, malformed requestの対処 - bokunonikki.net

など

  • config/puma.rbに正しいkeyとcertを記入したのに、bundle exec pumactl start -e productionすると'No such key file'と言われる

アクセス権限がないフォルダにkeyとcertが存在している場合があります。(特にcertbotで作った時とか) sudoで実行してみて動いたら、ファイルの権限を変更するとか、ファイルを移動するなどすれば解決できます。

railsアプリが動くようになるまでに必要なこと

[Rails]production環境で動かす - Qiita

  • log/production.logを見るとアセットパイプラインが読み込まれていない アセットパイプラインをプリコンパイルする必要があります。
bundle exec rake assets:precompile RAILS_ENV=production

RailsでPostgresqlを使用する - Qiita

ruby-on-rails - 環境変数を使用してpostgresqlのユーザ名とパスワードをdatabase.ymlでハードコーディングすることを避けるには?

postgresql.confの場所は/var/lib/pgsql/data/postgresql.conf

bundle exec rake db:create RAILS_ENV=production
bundle exec rails db:environment:set RAILS_ENV=production
bundle exec rake db:migrate:reset RAILS_ENV=production DISABLE_DATABASE_ENVIRONMENT_CHECK=1
rails db:seed RAILS_ENV=production

するとpostgresでデータベースが設定できる。

bundle exec pumactl start -e production

これでメールを送る前までは動く。 メールを送るには Rails の ActionMailer でメール送信処理 - Qiita パスワードというのはグーグルが発行する12桁のアプリパスワードのことで、作成には二段階認証を登録する必要がある。

これでメールが届くようになった。 アクティベーションのホスト名が違う

config/production.rb

config.action_mailer.perform_caching = false
  config.action_mailer.delivery_method = :smtp
host = 'naomiatlibrary.com'
  config.action_mailer.default_url_options = { host: host }
  ActionMailer::Base.smtp_settings = {
    address: 'smtp.gmail.com',
    domain: 'gmail.com',
    port: 587,
    user_name: 'gmail account',
    password: 'password',
    authentication: 'plain',
    enable_starttls_auto: true
  }

これで正しいアドレスを記載したメールが届くようになったが、アドレスにアクセスしてもアクティベーションされず、

The requested URL /account_activations/---/edit was not found on this server.

になる。 これはhost = 'naomiatlibrary.com:9292'にするべきだった。

bootstrapやfullcalender.ioが働いてない

f:id:Naomi_Lilienthal:20200816163633p:plain こんな感じ。 これを治すには

RailsアプリでCSSの変更が反映されない時の対処【アセットパイプライン】 - Qiita これで解決

カレンダーがうまく表示されない

f:id:Naomi_Lilienthal:20200816170908p:plain

 yarn add @fullcalendar/core @fullcalendar/daygrid @fullcalendar/list @fullcalendar/timegrid

してもダメだ…

→config/webpacker.ymlのextractcss:trueをfalseにした naomi-notebook.hatenablog.com

してもダメだ…もういっかい

bundle exec rake assets:precompile RAILS_ENV=production

したらできた!!

というわけでデプロイできました。

f:id:Naomi_Lilienthal:20200817211820p:plain

困ったこと

sudoをつけてbundle exec pumactl start -e productionしないとSSL routines:tls_post_process_client_hello:no shared cipherというエラーが出ます。SSL証明書の権限をchmodで変更したりSELinuxを無効化してみたりと色々したのですが、ついに直すのは無理でした。sudoで実行するのはあまり良くないと思うので避けたいのですが…(どうすればいいのだろうか) →できた

naomi-notebook.hatenablog.com


こんなのも見つけた(参考まで)

VPSでRailsアプリを動かすまでの全手順【Rails6, Capistrano, Puma, Nginx, MySQL, HTTPS】 - Qiita

自分用メモ herokuにpushするものとの変更点 * config/environments/production.rbのhostを変える * config/puma.rbに鍵を追加する

Ruby on Rails tutorial +alpha(3)生活記録を作成する

注意:この記事はメモなので、この記事に書いてあることをやっただけでは同じものができません。(大体の雰囲気は書いてあると思います。)

注意:全くのweb開発初心者なので、間違っているところなどがあるかと思いますが、教えていただけると嬉しいです。

naomi-notebook.hatenablog.com

続きです。eventを作成するフォームを作ります。

homeにもカレンダーを追加

前回はユーザページにしかカレンダーと生活記録が表示されませんでしたが、ホームから投稿した時にわかりやすいように自分の生活記録がホームに表示されるようにします。 ちょっと困ったのは、homeからuser/1/eventsにアクセスするのと同じなのはusersのshowからは1/eventsにアクセスしなければならないので、うまくカレンダー部分をパーシャルに分けられなかったことでした。とりあえずパーシャルに分けないで二回書いています。(教えてください…)

event追加フォームをつくる

ほとんどmicropostを作った時と同じで、roots.rbを編集し、createアクションをeventコントローラに追加した後、viewでフォームを作ります。

eventのidをすぐ知ることができるようにする

eventを削除するためには、簡単にeventのidを知ることができる必要があります。今回は、カレンダーをクリックした時にそのeventの編集・削除ページへ飛ばされるようにします。

Fullcalender.ioのeventclickで、イベントがクリックされた時にjsonで渡した情報を確認できます。 この機能で、イベントがクリックされた時には/events/(event_id)/edit に飛ばし、そこで記録の編集・削除を行うことにします。

eventClick: function(info) {
        window.location.replace("/events/"+info.event._def.publicId);
      } 

eventの編集フォームを作る

/events/:id/editを作ります。 コントローラー

def edit
        @event = Event.find(params[:id])
    end

    def update
        @event = Event.find(params[:id])
        if @event.update(event_params)
          flash[:success] = "生活記録が更新されました!"
          redirect_to root_url
        else
          render 'edit'
        end 
    end

ビュー

<h1>生活記録を変更</h1>

記録id:<%=@event.id%><br>
タイトル:<%=@event.title%><br>
タイプ:<%=@event.eventtype%><br>
開始時刻:<%=@event.starttime%><br>
終了時刻:<%=@event.endtime%><br>
<div class="row">
  <div class="col-md-6 col-md-offset-3">

<%= form_with(model: @event, local: true) do |f| %>
    <%= render 'shared/error_messages', object: f.object %>
    <%= f.label :title, "記録のタイトル" %>
    <%= f.text_field :title, class: 'form-control'%>

    <%= f.label :eventtype, "記録のタイプ(カレンダーでの色)"%>
    <%= f.number_field :eventtype %>

    <%= f.label :starttime, "開始時刻" %>
    <%= f.datetime_field :starttime, placeholder: Time.now.ago(30.minutes) %>

    <%= f.label :endtime, "終了時刻" %>
    <%= f.datetime_field :endtime, placeholder: Time.now %>

    <%= f.submit "変更", class: "btn btn-primary" %>
<% end %>

   
</div>
</div>

eventが削除できるようにする

deleteを送信してdestroyアクションを実行するボタンを作ります。 ビュー

<%= link_to "削除", @event, method: :delete,data: { confirm: "この生活記録を消しますか?" }, class: "btn btn-danger" %>

コントローラ

def destroy
    Event.find(params[:id]).destroy
    flash[:success] = "行動記録が削除されました"
    redirect_to root_url
  end

これで削除ができるようになります。

バグの修正

いじっているうちに、micropostやeventでinvalidな投稿をするとundefined method `errors' for nil:NilClassエラーが出るようになってしまいました。 とりあえずは以下で治りました。

<% if object && object.errors.any? %>

が、別のエラーが発生… micropostかeventの投稿に失敗した後、もう一方で投稿しようとするとエラーが出ます。 form_withのmodelによるurl予測がうまくいっていないようだったので、form_with(url:events_path,method: :post)にしました。そうするとparams[:events]がなくなるので、require(:events)を消します。そうするとupdateの時にeventsの下にデータがあるので失敗してしまいます。updateの方は別の関数を作ることで解決しました… これでエラーは出なくなりました。 が、制限に反する入力をした時にエラーメッセージが出なくなってしまいました…なぜだろう…

とりあえずflashにメッセージを入れて、それをエラーメッセージとすることにしました。

セキュリティーの追加・テストを書く

最後にテストを書きながら、権限の制約を追加していきます。 確認することは以下です。

・users/(user_id)/と、users/1/events/へは、ログインしてなければアクセスできず、ログイン画面にリダイレクトされる

test "usersページには、ログインしていないときはリダイレクトする" do
    get user_path(@user)
    assert_redirected_to login_url
  end

  test "eventsページには、ログインしていないときはリダイレクトする" do
    get user_path(@user)+"/events"
    assert_redirected_to login_url
  end
before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :following, :followers,:events,:show]

・(そのuserを)フォローしているアカウントじゃないとevents/:idが見られない

require 'test_helper'

class EventsControllerTest < ActionDispatch::IntegrationTest
  def setup
    @user = users(:michael)
    @following_user = users(:lana)
    @not_following_user = users(:archer)
    log_in_as(@user)
  end
  test "users/1/eventsを自分が閲覧できる" do
    get user_path(@user)+"/events"
    events = JSON.parse(@response.body)
    assert_equal "食事", events[0]['title']
  end
  test "users/1/eventsをフォローしているユーザは閲覧できる" do
    get user_path(@following_user)+"/events"
    events = JSON.parse(@response.body)
    assert_equal "睡眠", events[0]['title']
  end
  test "users/1/eventsをフォローしていないユーザは閲覧できない" do
    get user_path(@not_following_user)+"/events"
    assert_redirected_to root_url
  end
end
before_action :follower_user,   only: [:events]
...
private
def follower_user
      @user = User.find(params[:id])
      redirect_to(root_url) unless @user.followers.include?(current_user) || current_user?(@user)
    end

・同じアカウントじゃないとeventが作成できない

・eventを作ったのと同じアカウントじゃないとevent編集画面に行けない

・eventを作ったのと同じアカウントじゃないとeventが編集できない

・同じアカウントじゃないとeventが削除できない

ここはmicropostを作った時のテストを参考に書きます

できました。 f:id:Naomi_Lilienthal:20200815165945p:plainf:id:Naomi_Lilienthal:20200815165949p:plainf:id:Naomi_Lilienthal:20200815165954p:plain

Ruby on Rails tutorial +alpha(2)カレンダーを表示する

注意:この記事はメモなので、この記事に書いてあることをやっただけでは同じものができないかもしれません。(大体の雰囲気は書いてあると思います。) 注意:全くのweb開発初心者なので、間違っているところなどがあるかと思いますが、教えていただけると嬉しいです。

書いていなかったと思いますが、カレンダーを表示して、そこで何か生活習慣を整えられるようなアプリを作りたいので、まずはカレンダー上に睡眠状況が表示できるようにします。

git checkout -b add_calendar

カレンダーの表示

調べたところsimple calendarというgemが存在するらしいので(なんでもありますね…)それを使おうと思ったのですが、やってみたところ一週間を大きく表示したり時間毎の予定の表示なんかはできないみたいです。のでfullcalendarを使います。

github.com

readmeを読んでその通りにやったのですが、カレンダーが出ない… asserts/javascripts/application.jsがrails 6では存在しないようです。

う〜ん。(色々やってみたけどうまくいかなかった) まあ練習代わりにも、以下のようなものを参考にjQueryで書くことにしました。

fullcalendar.io stackoverflow.com

yarn add @fullcalendar/core @fullcalendar/daygrid @fullcalendar/list

app/javascript/packs/application.js

window.Calendar = require("@fullcalendar/core").Calendar;
window.dayGridPlugin = require("@fullcalendar/daygrid").default;
window.listPlugin = require("@fullcalendar/list").default;

app/javascript/stylesheets/application.scss

@import '../node_modules/@fullcalendar/core/main.css';
@import '../node_modules/@fullcalendar/daygrid/main.css';
@import '../node_modules/@fullcalendar/list/main.css';

views/layouts/~~~

<div id="calendar"></div>

...
<script>
  $(function() {
    var calendarEl = document.getElementById('calendar');
    var calendar = new Calendar(calendarEl, {
      header: {
        left: 'prev,next',
        right: 'dayGridMonth, listMonth'
      },

      plugins: [ dayGridPlugin, listPlugin ],
      defaultView: 'dayGridMonth'
    });

    calendar.render();
  });
</script>

みられました。 使用目的としては時間でグリッドされたカレンダーを見せたいですね。 TimeGrid View - Docs | FullCalendar timegridを追加すればできます。

イベントモデルの作成

次は日付・時間にそって睡眠記録などが表示されるようにします。 まず、イベントというモデルを作ります。

 rails generate model Event type:integer title:text starttime:datetime endtime:datetime user:references

要件としては、

  • Titleはnullでも大丈夫、20文字以内(適当)
  • typeはnullでも大丈夫(特に制限がない)
  • starttime,endtime,user_idはnilではいけない
  • starttime<endtimeである

です。 また、user_idとstarttime,endtimeで検索することが多いので、userとstarttime,endtimeにインデックスをつけておきます。

class Event < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
  validates :title, length: { maximum: 20 }
  validates :starttime, presence:true
  validates :endtime, presence:true
  validate  :end_day_is_after_start_day

  def end_day_is_after_start_day
    errors.add(:end, "はstartより後の日時を指定してください") if starttime==nil || endtime==nil || starttime > endtime
  end
end

ところでこれ、タイムゾーンが東京ではないのでめんどくさそうですね…国際化は後で考えるとして、とりあえずconfig.time_zone = 'Tokyo'をapplication.rbに追加しておきました。

また、userの方にもhas_many :events, dependent: :destroyを追加しておきます。

次に適当に有効性testを書きます。テストを書いているとエラーが出ました。

ActiveRecord::SubclassNotFound: The single-table inheritance mechanism failed to locate the subclass: '1'. This error is raised because the column 'type' is reserved for storing the class in case of inheritance. Please rename this column if you didn't intend it to be used for storing the inheritance class or overwrite Event.inheritance_column to use another column for that information.

どうやらtypeという名前はすでにrailsによって使われているからダメみたいですね。eventtypeに改名して進めました。

jsonでの出力・

jsonを渡すとカレンダー上でイベントを表示できるということで(Event Object - Docs | FullCalendar)、そのjsonをuser.eventsから作ってもらうことにしましょう。

次に、users/(user_id)/event/にアクセスすると、jsonでイベント情報を出すようにします。

Users_contoroller.rbに

def events
    @title = "Events"
    @user  = User.find(params[:id])
    @events = @user.events
    render 'events',formats: :json , handlers: 'jbuilder'
  end

を追加します。formats: "json"にするとrails5以前では動くようですが、rails6ではArgumentError: Invalid formats: "json" エラーになるので注意してください。

あとはこれをカレンダーに読ませれば完了です。(詳しくはFullcalender.ioのページを見てください。)

events: <%=@user.id%>+"/events"

できたもの f:id:Naomi_Lilienthal:20200814165558p:plain

次回予告

以下のような改善を今の所は考えています。 今後はブログに書くのは、書いても支障がなさそうなものを選んで書くことにします。

機能として必ず必要なもの

  • 当たり前ですが、eventを作成する画面を作る(とりあえずフォームで、それができたらもっと優れたやりやすい方式で)
  • eventを後から変更する画面を作る

使いやすさの面で必要そうなもの

  • eventをタイマーなどでリアルタイムに作成できるようにする(studyplusアプリのアレみたいなイメージです)
  • カレンダー上の予定をクリックすると編集画面が出てくる

個人情報の扱い的に正しそうなもの

  • ログインした時やフォローされている時しかevents/(user_id)/eventsでjsonデータが返されないようにした方が個人情報的には良さそうです
  • 上に付随して、フォローされるのを許可制にできるようにする
  • 誰にでも見せられる設定にしている時、userのプロフィール画面に簡単にはアクセスできないようにする(アドレスを複雑にする)ことでアドレスを知っている人にだけ記録が見せられるようにする

スケーリングのために必要そうなこと

  • fullcalendarが/users/(user_id)/eventsにアクセスするときに全てのuser.eventsを取得するのではなく、endtimeがstartより後、またはstarttimeがendより前のもののみ取得する(whereとかを使えそう)

その他

Ruby on Rails tutorial +alpha (1)ユーザー画像をGraphbizじゃなくてデータベースに保存できるようにする

第一回:ユーザー画像をGraphbizじゃなくてデータベースに保存できるようにする

チュートリアルをやったとはいえまだ全然わからんなので、とりあえず既存の知識の組み合わせですぐできそうなものを。 やっていきます。

git checkout -b user-image

userとactivestorageを結びつける (app/models/user.rb)

class User < ApplicationRecord
    has_one_attached :image

rails db:migrateする

viewにimageのアップロード場所を作る

<%= f.label :image, "User Picture" %>
      <span class="image">
        <%= f.file_field :image %>
      </span>

そのほかuser_contoroller

def update
    @user = User.find(params[:id])
    #画像の追加
    @user.image.attach(params[:user][:image])
    if @user.update(user_params)
      flash[:success] = "Profile updated"
      redirect_to @user
    else
      render 'edit'
    end 
  end
...
def user_params
      params.require(:user).permit(:name,:email,:password,:password_confirmation,;image)
    end

多分これで内部的には画像が保存できてるはず。 あとはgravater_forを変えて保存してある画像を表示するようにする。 gravater_forを全体的に

<%= image_tag current_user.image if current_user.image.attached? %>

などに変えればOK。 さらに、micropostでやったように5MB以下とかファイル形式の制限をつけるとよい。

ただしこのままだとサイズがそのまま表示されてしまう。 これもImageMagicを使ってやればいいだけ。

 def icon_image(size:80)
    return image.variant(resize_to_limit: [size, size])if image.attached?
    return 'USAGINEKO.jpg'#,size:"#{size}x#{size}"
  end

デフォルト画像を用意したのだが、これのサイズも関数の中で指定する方法がわからなかった。しょうがないのでimage_tagの方で指定してあるが、やり方が知りたい。

取り急ぎできた(とても雑だが)

f:id:Naomi_Lilienthal:20200812225032p:plain
結果