Ryota400’s blog

エンジニアを目指して書いてます。

whenever による記事数一覧のメール送信

実装したいこと

・wheneverを導入して毎日am9:00に下記の内容を管理者にメールで送信させるメールの件名には「公開済記事の集計結果」と設定

・管理者のメールアドレスにはadmin@example.comを設定

Image from Gyazo

Mailer作成

記事数一覧メールを送信できるように「送信するためのメソッド」と「メール本体」を作成

rails g mailer ArticleMailer

・ActiveMailer::Baseを継承したApplicationMailerとArticleMailer(自分で命名したもの)が生成される。

・Mailerを作成するとviewのディレクトリとテストも同時に作成される。

app/mailer/application_mailer.rb

class ApplicationMailer < ActionMailer::Base

  default from: 'from@example.com'

  layout 'mailer'

end

mailerのviewでHTMLタグなどを書かないこと
他のViewファイル同様レイアウトファイルが指定されているから 書いてしまうと重複になってしまう。

app/mailers/article_mailer.rb

class ArticleMailer < ApplicationMailer
  def report_summary
    @published_article_count = Article.published.count
    @articles_published_at_yesterday = Article.published_at_yesterday
    mail(to: 'admin@example.com', subject: '公開済記事の集計結果')
  end
end

・@publish_article_countは全ての記事の中で公開されている記事の数を出しているもの
・@article_publish_at_yesterdayは全ての記事の中で公開日が昨日のものを表示している。
・admin@example.comに送る設定で、題名は「公開済記事の集計結果」
・Article.published_at_yesterdayのpublished_at_yesterdayの部分はscopeを使い指定してやる必要がある(そうじゃないと使えない)

article.rb

 scope :published_at_yesterday, -> { where(published_at: 1.day.ago.all_day) }

@publish_article_count=公開記事の総数と@article_publish_at_yesterday=昨日記事を公開した記事の情報をメールに表示することができるようになる。

メールを書く
app/views/article_mailer/report_summary.text.erb

公開済の記事数: <%= @published_article_count %>件

<% if @articles_published_at_yesterday.present? %>
  昨日公開された記事数: <%= @articles_published_at_yesterday.count %>件
  <% @articles_published_at_yesterday.each do |article| %>
    タイトル: <%= article.title  %>
    公開日時: <%= l(article.published_at) %>
  <% end %>
<% else %>
  昨日公開された記事はありません
<% end %>

wheneverを使って毎朝9時に「記事数一覧メールを送信する」というメソッドを実行
config/schedule.rb

every 1.day, at: '9am' do
  rake 'article_summary:mail_article_summary'
end
 bundle exec whenever --update-crontab 

最後に変更をcronに反映させる

メール送信の定期実行

lib/tasks以下にcronを使用して、定期的に実行する処理のかたまり(rakeタスク)を作成していく lib/tasks/article_summary.rake

namespace :article_summary do
  desc '管理者に対して総記事数、昨日公開された記事数とタイトルをメールで送信'
  task mail_article_summary: :environment do
    ArticleMailer.report_summary.deliver_now
  end
end

・namespaceはfile名と揃える(自分で作成)
・"desc"はこのタスクの説明(自分で作成)
・"task" の "mail_article_summary:" は自分で命名していい。config/suchedule.rbにも同じ名前を記述することになる。

letter_opener gem

開発環境で送信したメールをブラウザで確認することができる。

環境設定
Gemfileにletter_opener_webを追加

group :development do
  gem 'letter_opener_web'
end

config/environments/development.rb

  config.action_mailer.delivery_method = :letter_opener_web
  config.action_mailer.default_url_options = { host: 'localhost:3000' }

route.rb

if Rails.env.development? #開発時用の処理
  get '/login_as/:user_id', to: 'development/sessions#login_as' #開発環境でログインする為
  mount LetterOpenerWeb::Engine, at: '/letter_opener' #今回ここ追加
end

これで、http://localhost:3000/letter_opener/で確認することが可能

参考文献

wheneverを使って定期的にメールを送信する方法 - Qiita

【Rails】開発中に送ったメールを確認する(Gem: letter_opener) - おぴよの気まぐれ日記

GitHub - javan/whenever: Cron jobs in Ruby

Rails メール自動配信機能をActionMailerとwheneverを使用して実装する - Qiita

WEb技術基本

WEB技術とは

webの正式名称は World Wide WEB → 世界に広がるクモの巣 → 略してweb
web上の文書はハイパーテキストという言語で構成され、ハイパーリンクで繋がる
1つのwebページを、複数のwebページと関連付けることで、全体で大きな情報の集合とすることができる

・1つのドメインにある複数のweページの集まりをwebサイトと呼ぶ
ハイパーテキストを記述するための言語 HTML
・「タグ」で意味づけされて書いたものを人間が読みやすいように作り変えて表示してくれるのがwebブラウザ

WEBサーバー

webブラウザからの要求があると必要なコンテンツをブラウザに送信する役割 → 要求されたコンテンツがない場合、「無いですよ」で返答
→ 別のサーバーに要求するよう案内することも役割の一つ

HTTP(Hyper Text Transfer Protocol)

正解共通の仕様として決められたハイパーテキスト(コンテンツ)のやりとりの手順
送信手順だけでなく、要求されたコンテンツを持っていなかった場合の応答方法も含む
webサイトの移転を伝える、必要となる様々な手順を定義
正解共通の基準なので、どの種類のブラウザでもあらゆる種類のサーバーとの間で同じ手順でやりとりができる

webページ表示の流れ

1,HTTPを使ってアクセス
2,(例)www.aaaaa.jpという名前のwebサービスにアクセス
3,(例)cccc.htmlという名前のコンテンツを表示

静的
毎回同じものが表示される
製作者が更新しないと変化なし

メリット
①動的ページに比べるとセキュリティ対策が簡単。
②ページの表示速度が早い。
③サーバーダウンが起こりにくい。(同時アクセスが多すぎる場合)

デメリット
①更新の手間が掛かる。情報をすぐ更新しづらい。
②ユーザーごとに異なる情報を表示できない。

動的
検索した文字列に対し最新の検索結果を表示
コンテンツが随時追加される
ユーザーが書き込む度に結果が増える掲示板など

メリット
①常に最新の情報を表示できる。
②ユーザーの要求に応じて異なる情報を表示可能。
③更新が簡単。(CMSの場合など)
④結果的にコスト削減できる場合が多い。

デメリット
上手く制作しないと、Webページの表示スピードが遅くなる。

動的ページの仕組み
CGI common gateway interface
→webサーバーが、ブラウザからの要求に応じてプログラムを起動させるための仕組み

サーバーサイドスクリプト
CGIから呼び出されるプログラム
→一般的に文字列の扱いに長けたスクリプト言語で記述される
例) perl, Ruby, Python, PHP, javascript

クライアントサイド・スクリプト HTMLに埋め込まれ、ブラウザで読み込まれる際に起動する サーバーサイド・スクリプトの対になるもの 例) Javascript

Webの設計思想

RESTの原則

統一インターフェース
あらかじめ定義・共有された方法で情報がやりとりされる
例) HTTPを使います

アドレス可能性
すべての情報が一意なURL構文で示される
例) index.htmlのファイルをください。遷移先のURLを表示させる。

接続性
やりとりされる情報にはリンクを含めることができる
例) リンクをクリックしてその先のページに移動

ステートレス性
やりとりは1回ごとに完結し、前のやりとりの結果に影響を受けない
例)さっきの指示をもう一度してください→さっきの情報がわかりません

RESTの原則を守ったシンプルな設計のことを、 RESTfulなシステムと呼ぶ。APIの相互運用が円滑になる。

セマンティックWeb
ティム・バーナーズ=リーが提唱
webページの情報に意味(セマンティック)を付け加えたもの
XML文書の中にRDF言語で意味を記述。言葉の相互関係などはOWL言語で記述。
情報に関する意味を示す情報を、メタデータと呼ぶ。W3Cで標準化が進められている。

例) 住所が"埼玉",院長の名前が"埼玉" これを区別する  

"webアプリケーション"と"webシステム"の違い

webページとは
web上にある文書のこと

webサイトとは
特定のドメインの下にあるwebページの集まりのこと

webアプリケーションとは
webを介して人が利用するサービスを提供するもの
例)ショッピングサイト、ネットバンキングなど

webサービス
プログラムが利用するサービスを提供する
例) プログラムAPI

webを実現するコンピューターネットワーク

コンピューターネットワークとは
PC同士がお互いに接続して情報のやりとりをする仕組み

サーバー
ネットワーク上で情報やサービスを提供する役割を持つコンピューター

クライアント
サーバーから提供された情報やサービスを利用する役割を持つコンピューター
・インターネット接続にはプロバイダーが必要
・プロバイダー同士が接続することで世界中が1つのネットワークとして成立。

インターネットの標準プロトコル

プロトコル
機器同士が通信するときの決められたルールと手順
例) HTTPなど

TCP/IP ( transmisson contorl protocol / internet protocol ) プロトコルの集まりのこと
インターネットに接続可能な機器はすべて対応している。スマホも。

プロトコルの種類】
HTTP:webブラウザとサーバー
FTP:PC間でのファイルのやりとり
SMTP:電子メール送信用
POP:メール受信用

IPアドレスとポート番号

IPアドレス
コンピューターを特定、データの行き先管理
IPアドレスは必ず1つ = 住所のようなもの
32ビット、IPv4、10進数で表記
例) 192. 168. 1. 1

グローバルIP
ネットでの利用、ICANN管理
プライベートIP:LAN内での利用、個人・会社で自由
電話番号でいう、外線と内線のイメージ

ポート番号
PCの何を起動するかを特定する

【捉え方のイメージ】 コンピューター→マンション
IPアドレス→住所
ポート番号→部屋番号

URLとドメイン

例 http://example.com:80/test.html

1.スキーム名(http:):プロトコルを指定
主なスキーム名
http: WEBサイトを閲覧する際に利用されるプロトコル
https: httpを暗号化(SSL)しているプロトコル
ftp: ファイル転送で使用するプロトコル

2.ホスト名(example.com):接続先のドメイン
3.ポート番号(80):接続先のサーバーの指定。通常は省略される。決まっているため。
4.パス名(test.html):ディレクトリやファイルの指定

DNSとは

ドメインIPアドレスに変換する仕組み
(例)www.sbcr.jp(ドメイン) ---> 18.182.225.171(IPアドレス)
DNSドメインIPアドレスが紐付いて管理されている

HTTPメソッド

Image from Gyazo

GETとPOSTの違い
GETリクエストの内容はURLに含まれ、HTTPS通信でもその内容は暗号化されることはありません。
そのため、他人に見せたくない情報はGETリクエストでは送りません。

POSTの内容はURLには含まれず、リクエストボディに含まれます。
HTTPS通信の場合は暗号化されるので、リクエスト内容が他の人に漏れることはありません。

HTTPリクエストに対しサーバー内での処理結果

ステータスコードの5分類

100:リクエスト継続を通知
200:正常
301:リクエストされたコンテンツは移動しました
302:一時的に移動された
304:コンテンツ未更新、ブラウザに一時保存されたコンテンツが表示
400:リクエストが不正
404:コンテンツ未検出、ページが見つかりません
500:リクエスト処理中にサーバー内部でエラー発生
503:アクセス集中やメンテナンスで処理不可

HTTPSのやりとり

HTTPS通信では、SSLを使用して暗号化を行いWebサイトの安全性を高めている
通信の暗号化とは、決められた鍵を使って通信に暗号をかけることを言い、鍵がないと暗号が解除できないような仕組み

SSLは、Webページのデータを暗号化するほかに、サイトの運営者情報を確認することができる「SSLサーバ証明書」の利用が可能です。SSLサーバ証明書にはサイトの運営者を登録する必要があり、ユーザはこの証明書を見れば、運営者が信頼のできる相手かどうかを確認することができる

(例) オンラインショッピングを利用する際、サイトにSSLサーバ証明書が使われていない通信では、通信の内容が不正に取得・改ざんされるリスクがあります。オンラインショッピングでは名前や住所、クレジットカード番号などの個人情報を入力することが多いですが、SSLを使用していないと悪意ある第三者に情報が漏れてしまう危険性が高まります。

Image from Gyazo

HTTPとHTTPSの違い

HTTPとHTTPSの主な違いは通信内容が暗号化されていないか、されているかの違い

Google ChromeではHTTPSに対応したホームページを開くとブラウザのURLの左の方に「保護された通信」と表示、また「保護された通信」ではなく、組織名が表示される場合もある。

Image from Gyazo

Image from Gyazo

ステートフルとステートレス

ステートフル
直前にやりとりした相手などの状態を、以降のやりとりでも覚えていること。
(例) ショッピングサイトの買い物かご、データを一時保存

ステートレス
毎回リセットするものをステートレス(前回の状態を覚えていない)

Cookie クッキー

クッキー(Cookie)とは、Webサイトの訪問者の情報を一時的に保存するための仕組みであり、それぞれのユーザーの識別を目的としている。
具体的には、ユーザーの購買履歴や、利用している地域などがわかる会員証のようなものとよく例えられます。2回目以降同じサイトに訪れる際は、初めてレスポンスを受けとる際に付与されたクッキー(会員証)を元にサーバーがブラウザを特定します。 クッキー(Cookie)はパソコンやスマートフォンに保存されますが、ファイルサイズが小さいため空き容量を減らす原因にはなりません。

セッション

クライアントとサーバーで通信を行う場合であれば、クライアントからサーバーへ接続した時点でセッションが始まり、サーバーから切断するとセッションが終了します。この一連の流れを管理することをセッション管理という

(例) 商品を選ぶ→かごに入れる→購入の流れをセッションと呼ぶ

参考文献

【Web技術の基本】知っているつもりの専門用語まとめ - Qiita

静的ページ・動的ページとは?

http・httpsとの違いは?ウェブセキュリティの基本を解説|サイバーセキュリティ.com

【初心者向け】クッキーとキャッシュ。それぞれの違い、役割とは | ワードプレステーマTCD

トップ画像をスライダー形式に変更

ActiveStorageとは

Amazon S3Google Cloud Storage、Microsoft Azure Storageなどの クラウドストレージサービスへのファイルのアップロードや、ファイルをActive Recordオブジェクトにアタッチする機能を提供しています。
development環境とtest環境向けのローカルディスクベースのサービスを利用できるようになっており、ファイルを下位のサービスにミラーリングしてバックアップや移行に用いることもできる。

Active Storageの実装

# マイグレーションファイルを作成
rails active_storage:install 

# マイグレーションファイルを実行
rails db:migrate

Active Storageを使って、複数画像ファイルをアップロード

site.rb

class Site < ApplicationRecord
    # Active Storage の「1対1」の画像アップロード
  has_one_attached :og_image
  has_one_attached :favicon

    # Active Storage の「1対多」の画像アップロード
  has_many_attached :main_images

・スライド画像用のパラメータがストロングパラメータに追加されていること
site_controller.rb

private

  def site_params
    params.require(:site).permit(:name, :subtitle, :description, :favicon, :og_image, main_images: [])#最後追加
  end

・各画像の削除ボタンが追加されていること
app/views/admin/sites/edit.html.slim

= image_tag @site.favicon_url('32x32')
        br
        br
        = link_to '削除', admin_site_attachment_path(@site.favicon.id),
          method: :delete, class: 'btn btn-danger'
        br
        br

      = f.input :og_image, as: :file, hint: 'JPEG/PNG (1200x630)'

        = image_tag @site.og_image_url(:ogp), class: 'img-responsive'
        br
        br
        = link_to '削除', admin_site_attachment_path(@site.og_image.id),
          method: :delete, class: 'btn btn-danger'
        br
        br
      = f.input :main_images, as: :file, input_html: {multiple: true}, hint: 'JPEG/PNG (1200x400)'

      - if @site.main_images.attached?
        .main_images_box
          - @site.main_images.each do |main_image|
            .main_image
              = image_tag main_image.variant(resize:'300x100').processed
              = link_to '削除', admin_site_attachment_path(main_image.id),
                method: :delete, class: 'btn btn-danger'

Active Storageでアップロードした画像を削除する

継承元を含めるコントローラーを生成するコマンド

bin/rails g controller Admin:Site:Attachments

admin/site/attachments_controller.rb

class Admin::Site::AttachmentsController < ApplicationController
  def destroy
    authorize(current_site)
    image = ActiveStorage::Attachment.find(params[:id])
    image.purge
    redirect_to edit_admin_site_path
  end
end

routes.rb

resource :site, only: %i[edit update] do
  resources :attachments, only: %i[destroy], controller: 'site/attachments'
end

モデルにバリデーションを作成

site.rb

 validates :og_image, attachment: { purge: true,content_type: %r{\Aimage/(png|jpeg)\Z}, maximum: 524_288_000 }
 validates :favicon, attachment: { purge: true,content_type: %r{\Aimage/(png|jpeg)\Z}, maximum: 524_288_000 }
 validates :main_images, attachment: { purge: true, content_type: %r{\Aimage/(png|jpeg)\Z},maximum: 524_288_000 }

app/validators/attachment_validator.rb

if options[:maximum]
      if value.is_a?(ActiveStorage::Attached::Many)
        # 画像が複数枚投稿された場合
        value.each do |v|
          unless validate_maximum(record, attribute, v)
            has_error = true
            break
          end
        end
      else
        # 画像が1枚投稿された場合
        has_error = true unless validate_maximum(record, attribute, value)
      end
end

if options[:content_type]
      if value.is_a?(ActiveStorage::Attached::Many)
        # 画像が複数枚投稿された場合
        value.each do |v|
          unless validate_content_type(record, attribute, v)
            has_error = true
            break
          end
        end
      else
        # 画像が1枚投稿された場合
        has_error = true unless validate_content_type(record, attribute, value)
      end
    end

・Siteの画像はadminユーザー以外は削除できないようにしておくこと
app/policies/site_policy.rb

def destroy?
    user.admin?
  end

Swiperとは

Swiperは「CSSやJS」を適用することで、画像のスライドを簡単に実装できる機能提供する「ライブラリ」です。

Swiper導入方法

設定方法として以下の3つあります。
①Swiperの公式サイトから必要なファイル(CSSやJS)をダウンロードしてアプリケーションに置いて、そいつを読み込む
CDNを使って毎回クラウド上に公開したファイルにアクセスして取得する
③npm、yarnのJSのパッケージ管理ツールを使用する

今回はnpmでSwiperをインストールする手順と読込例です

 npm install swiper

package.json

"dependencies": {
    "swiper": "^6.7.5",
}
``

config/initializers/assets.rb

Rails.application.config.assets.paths << Rails.root.join('node_modules')

node_modulesディレクトリまでのパスはデフォルトでは読み込んでくれないので、導入したnode_modulesディレクトリ以下のファイルを読み込むように設定を書きます。

app/asssets/javascripts/application.js

//= require swiper/swiper-bundle.js

上記のコードでnode_modules/swiper/swiper-bundle.jsファイルを application.jsファイルから読み込んでいます。


app/assets/stylesheets/application.css.scs

@import 'swiper/swiper-bundle';

application.css.scssファイルでnode_modules/swiper/swiper-bundle.cssファイルを読み込みます。  
どうして読み込む必要があるかというと、JSファイルはスライダーの動きを使えるコードがswiper-bundle.jsファイルに記載されているからです。  
また、デザインを当てるためにswiper-bundle.cssファイルを読み込む必要があります。  


## 必要なCSS

app/assets/stylesheets/admin.css.scss

.main_images_box { display: flex; .main_image { text-align: center; padding: 1rem; img { display: block; margin-bottom: 1rem; } } }

assets/stylesheets/application.css.scss

header { position: relative;

.swiper-container { img { width: 100%; height: 400px; object-fit: cover; } }

.blog-title { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; z-index: 10; a { color: white; } } }

参考文献

[https://swiperjs.com/get-started:title]


[https://weblog-tec.hatenablog.com/entry/2021/07/14/212732:title]


[https://www.webdesignleaves.com/pr/plugins/swiper_js.html:title]


[https://qiita.com/kenkentarou/items/bdf04d8ecab6a855e50f:title]

埋め込みメディアタイプにTwitterの追加

実装したいこと

twitterのツイートを埋め込みコンテンツとして選択できるようにする(タイムラインではない)
twitterが選択され、適切なURLが入力されたら、ツイートが表示されるようにする
youtubeに関しても、IDではなくURL(https://youtu.be/dZ2dcC4OnQE のような形式)を入力したら動画を表示できるようにする
カラム名は現状のままで構いません
・入力項目のラベル名も本来はURLに変えるべきではありますが自動テストの制約上IDのままにしてください
・貼り付けるべきURLはyoutube再生画面の「共有」を押した後に表示されるものとする。 ・ブロックヘッダーのアイコンもYoutube/Twitterと適切に切り替わるようにする
・ブロック追加画面のアイコンはYoutubeTwitterの二つが表示されるようにする

イメージ

Image from Gyazo

Image from Gyazo

Image from Gyazo

カラム準備

models/embed.rb

class Embed < ApplicationRecord

  enum embed_type: { youtube: 0, twitter: 1 }
#embed_typeでYoutubeかTwitterを選択する。

  validates :identifier, length: { maximum: 200 }
#identifierでURLを保存します。

  def split_id_from_youtube_url
    # YoutubeならIDのみ抽出
    identifier.split('/').last if youtube?
  end

end

説明

  def split_id_from_youtube_url
    # YoutubeならIDのみ抽出
    identifier.split('/').last if youtube?
  end

最初はgoogleyoutube投稿された全ての動画の全てに割り振ったURLの末尾の11桁のvideo_idと呼ばれる数字を抽出した。だが開始時間を指定したurlになると、末尾の11桁が変わってしまうので、splitを使用して「/」で区切りをつけて「last」=最後の部分のみ抽出する。

記事ブロックの見出しのアイコンもtwitter

app/decorators/article_block_decorator.rb

elsif embed?
      blockable.youtube? ? content_tag(:i, nil, class: 'fa fa-youtube-play') : content_tag(:i, nil, class: 'fa fa-twitter')
end

記事ブロック挿入画面のアイコンもtwitter

app/views/admin/articles/article_blocks/_insert_block.html.slim

.d-inline-flex
   i.fa.fa-youtube-play
   i.fa.fa-twittertter

Twitterカード表示部分の実装

app/views/shared/_embed_twitter.html.slim

script async="" charset="utf-8" src="https://platform.twitter.com/widgets.js"
.embed-twitter
  blockquote.twitter-tweet
    a href="#{embed.identifier}"

Youtube埋め込みブロックのsrc部分を動的に指定

app/views/shared/_embed_youtube.html.slim

.embed-youtube
  = content_tag 'iframe', nil, width: width, height: height, src: "https://www.youtube.com/embed/#{embed.split_id_from_youtube_url}", \
    frameborder: 0, gesture: 'media', allow: 'encrypted-media', allowfullscreen: true

/ Youtube公式に乗っている埋め込み用のHTMLを出力する形にする。
/ #{embed.identifier.last(11)}でIDを切り取ると、開始秒指定されたときにOUT

app/views/admin/articles/article_blocks/_show_embed.html.slim

.box-body
  - if embed.identifier?
    - if embed.youtube?

      = render 'shared/embed_youtube', embed: embed, width: 560, height: 315
    - if embed.twitter?
      = render 'shared/embed_twitter', embed: embed

lib

複数のアプリケーション間で共有するライブラリを格納するディレクト

lib/tasks/temp/fix_embed_youtube_identifier.rake

namespace :fix_embed_youtube_identifier do
  desc 'IDを入力していたidentiferカラムの過去データを一括で修正'
  task update_old_identifier_for_youtube_embed: :environment do
    Embed.youtube.each do |embed|
      embed.update(identifier: "https://youtu.be/#{embed.identifier}")
    end
  end
end

参考文献

qiita.com

webukatu.com

アイキャッチの表示サイズ / 位置指定

実装したいこと

アイキャッチ画像をユーザーの入力した幅に設定したい。

アイキャッチ画像をユーザーの選択した位置に設定したい。

Image from Gyazo

アイキャッチとは

投稿ページの記事タイトルの下に表示される画像のこと

画像の表示サイズの設定

カラム追加

rails g migration AddColumnToArticles  eyecatch_width:integer#画像幅 eyecatch_align  :integer#画像位置
class AddImageInfoToArticles < ActiveRecord::Migration[5.2]
  def change
    add_column :articles, :eyecatch_align, :integer, default: 0, null: false
    add_column :articles, :eyecatch_width, :integer
  end
end

eyecatch_width (横幅用のカラム)のデフォルトを100にするが、カラムにデフォルトを追加すると変更するときに大変になるので、ここでは設定しない。

ayacatch_align (位置用のカラム) defaul:0やnul:falseを追加することで、記事作成でバリテーションエラーとなる。

rails db:migrate

モデルに対して Active Storage用の設定を追加

<一つのファイルを選択する>

has_one_attached :カラム名

<複数のファイルを選択する>

has_many_attached :カラム名

<今回の場合>
article.rb

has_one_attached :eye_catch

enum設定

enum eyecatch_position: { left: 0, center: 1, right: 2 }
#上記で左寄せ・中央寄せ・右寄せの3択で設定

Validation設定
articles.rb

validates :eyecatch_width, numericality: { less_than_or_equal_to: 700, greater_than_or_equal_to: 100 }, allow_blank: true

ユーザーの入力値は、100~700pxに限定します。 空欄を許可しておかないと、アイキャッチ画像を設定せずに更新したいときにもバリデーションエラーになるので必須の設定

・less_than_or_equal_to
指定された値と等しいか、それよりも小さくなければならないことを指定する。

デフォルトで「must be less than or equal to %{count}」というエラーメッセージを返す。

・greater_than_or_equal_to:
指定された値と等しいか、それよりも大きくなければならないことを指定する。

デフォルトで「must be greater than or equal to %{count}」というエラーメッセージを返す。

・allow_blank: true
値が空の場合はバリデーションをパスする。

入力フォーム作成

articles/edit.html.slim

            = image_tag @article.eye_catch_url(:thumb), class: 'img-thumbnail'
            br
            br
      = f.input_field :eyecatch_align, as: :radio_buttons    #デフォルトの100。ここでvalidatesでallow_blank: trueにしているので、blankの際は100が入る。
      = f.input :eyecatch_width, placeholder: '100'

このようなHTMLが作成される。

<span class="radio radio radio">
<label for="article_eyecatch_position_left"><input class="enum_radio_buttons optional" type="radio" value="left" name="article[eyecatch_position]" id="article_eyecatch_position_left" />左寄せ
</label></span>
<span class="radio radio radio">
<label for="article_eyecatch_position_center"><input class="enum_radio_buttons optional" type="radio" value="center" checked="checked" name="article[eyecatch_position]" id="article_eyecatch_position_center" />中央寄せ
</label></span>
<span class="radio radio radio">
<label for="article_eyecatch_position_right"><input class="enum_radio_buttons optional" type="radio" value="right" name="article[eyecatch_position]" id="article_eyecatch_position_right" />右寄せ
</label></span>

画像表示部分のclassを動的に変更

views/shared/_article.html.slim

- if article.eye_catch.attached?
    section class="eye_catch text-#{article.eyecatch_align}"    #enumを使用し位置指定
        = image_tag article.eye_catch_url(:lg), class: 'img-fluid', width: article.eyecatch_width      #eyecatch_widthを使用して横幅指定

class="text-right" class="text-center" class="text-left"を使用すれば、enumの値をそのまま使用できる。
画像幅は、保存した値をarticle.eyecatch_widthでそのまま取ってこれる。

Controller設定

(admin/articles_controller.rb)

def article_params
    params.require(:article).permit(
      :title, :description, :slug, :state, :published_at, :eye_catch, :category_id, :eyecatch_position, :eyecatch_width, :author_id, tag_ids: []
    )
# 上記に:eyecatch_positionと :eyecatch_widthを追記
  end

日本語設定

activerecord.ja.yml

 eyecatch_align: '位置'
 eyecatch_width: '横幅'

enums.ja.yml

   eyecatch_align:
        left: '左寄せ'
        center: '中央'
        right: '右寄せ'

参考文献

bon-voyage23.hatenablog.com

アクション権限の調整

実装したこと

・記事投稿アプリで、管理者や編集者以外は記事のCRUD機能を使用できないようにしたい。

・権限のないユーザーが該当のページにアクセスしたときは、403エラー画面を表示させる。

実装の流れ

・Punditの導入 ・policyファイルの設定 ・Controller設定 ・エラー画面設定

前提

role.rb

enum role: {
      writer: 0,
      editor: 10,
      admin: 20
    }

Pundit

Punditは、リソースに対して、どのユーザーであれば処理が許可されるのかを定義するもの

インストール
Gemfile

gem "pundit"
bundle install

application controller

class ApplicationController < ActionController::Base
  include Pundit
end

application controllerにPunditをincludeにする。

rails g pundit:install

generatorを走らせると、policyファイルを生成

Policyファイル
Policyファイルに認可を与えるユーザーについて設定していきます。
今回はadminとeditorに認可を与える

app/policy/article_policy.rb

class TaxonomyPolicy < ApplicationPolicy
  def index?
    user.admin? || user.editor?
  end

  def create?
  end

  def update?
    user.admin? || user.editor?
  end

  def destroy?
    user.admin? || user.editor?
  end

end

クラスの継承

クラスを継承している場合、継承元のPolicyに認可の設定をすれば、継承先でも適用されます。

エラー画面設定

publicディレクトリに403.htmlファイルをセット public/403.html

<!DOCTYPE html>
<html>
<head>
  <title>権限がありません(401)</title>
  <meta name="viewport" content="width=device-width,initial-scale=1">
</head>

<body>
<p>権限がありません。</p>
</body>
</html>

config/application.rb

config.action_dispatch.rescue_responses["Pundit::NotAuthorizedError"] = :forbidden

config/development.rb

config.consider_all_requests_local = true

development環境ではconfig/development.rbで上記のような指定がされています。
この設定が「true」になっていると、いつも見るようなエラーの画面が表示されます。
開発環境ではこの画面が表示されることでデバッグができるので便利です。

config.consider_all_requests_local = false

もし開発環境でも「本番環境」と同じようにエラー用の画面を表示したい場合は「false」に変更して、サーバーを再起動します。
そうすれば、「ルートディレクトリ/public/」配下のエラー用の画面を表示できます。
なので、「404」エラーが発生した場合に、public配下に「404.html」みたいにエラー用の画面が必要です。

参考文献

qiita.com

ログイン - はてな

magazine.rubyist.net

検索機能の追加

実装したいこと

・著者、タグ、コンテンツ(記事内容)に関しても検索が行えるようにする
・著者、タグはセレクトボックスによる選択、コンテンツ(記事内容)はフリーワード検索が行えるようにする
・追加する各検索機能では、下書き状態の記事も検索できるように実装

Image from Gyazo

ActiveRecordとは

データベースとのやりとりを行うための機能を提供します。
ActiveRecordでは、RDBのテーブルに対応するモデルクラスにアプリケーションのデータとロジックを実装していきます。

データの永続化に関する処理の多くをこのActiveRecordが担当します。

ActiveModel::Modelとは

ActiveRecordのデータベースに関連する部分以外の機能を切り出したもので、
これを利用することで、DBを利用しないフォームでもActive Recordを利用したときと同じような記述をすることができます

ActiveModel::Attributesとは

ActiveAttributesを利用することで、ActiveRecordと同じように属性の型の指定と変換を行うものです。

FormObjectとは

form_withのmodelオプションにActive Record以外のオブジェクトを渡すパターンのことです。(今回の場合ActiveModelなど)
単体のモデルに依存しない場合や
フォーム専用の特別な処理をモデルに書きたくないときに使われます。

メリット
・DBを使わないフォームでも、Active Recordを利用した場合と同じお作法を利用できるので可読性が増す

・他の箇所に分散されがちなロジックをform object内に集めることができ、凝集度を高められる

Viewの設定

app/views/admin/articles/index.html.slim

= form_with model: @search_articles_form, scope: :q, url: admin_articles_path, method: :get, html: { class: 'form-inline' } do |f|
            => f.select :category_id, Category.pluck(:name, :id)  , { include_blank: true }, class: 'form-control'
            => f.select :author_id, Author.pluck(:name, :id) , { include_blank: true }, class: 'form-control'
            => f.select :tag_id, Tag.pluck(:name, :id) , { include_blank: true }, class: 'form-control'
            .input-group
              = f.search_field :title, class: 'form-control', placeholder: 'タイトル'
            .input-group
              = f.search_field :body, class: 'form-control', placeholder: "本文"
              span.input-group-btn
                = f.submit '検索', class: %w[btn btn-default btn-flat]

フォームに入力された値がSearchArticleFormクラスに入り、インスタンスを生成するという形になっている。

コントローラの設定

app/controllers/admin/articles_controller.rb カテゴリーの追加

private

def search_params
    params[:q]&.permit(:title, :category_id, :author_id, :body, :tag_id)
  end

Formの設定

検索のロジックを書くapp/forms/search_article_form.rbをいうファイルを作成し、その中でActiveModel::Modelをincludeします。 コントローラーから切り出すことでFatControllerを防ぐ。

app/forms/search_article_form.rb

class SearchArticlesForm
  include ActiveModel::Model #ActiveRecodeと同じようにコードがかけるようになる。
  include ActiveModel::Attributes

#ActiveModel::Attributesで属性を定義する。
  attribute :category_id, :integer
  attribute :author_id, :integer
  attribute :tag_id, :integer
  attribute :title, :string
  attribute :body, :string

  def search
    relation = Article.distinct

    relation = relation.by_category(category_id) if category_id.present?
    title_words.each do |word|
      relation = relation.title_contain(word)
    end
    relation = relation.by_author(author_id) if author_id.present?
    relation = relation.by_tag(tag_id) if tag_id.present?
    body_words.each do |word|
      relation = relation.body_contain(word)
    end
#relationを記載しないと上記の部分で評価されたものが戻り値になる。
#結果として[]が戻り値になる。なのでrelationを記載し最後に評価することで、relationが戻り値となるようにする。

    relation
  end

  private

  def title_words
    title.present? ? title.split(nil) : []
  end

  def body_words
    body.present? ? body.split(nil) : []
  end
end

scope

<使用方法>

class モデル名 < ApplicationRecord
  scope :スコープ名, -> { 条件式 }
end

ここで定義した「スコープ名」をarticle.スコープ名のようにメソッドとして使える。

article.rb

scope :by_category, ->(category_id) { where(category_id: category_id) }
scope :title_contain, ->(word) { where('title LIKE ?', "%#{word}%") }
scope :by_author, ->(author_id) { where(author_id: author_id) }
scope :by_tag, ->(tag_id) { joins(:article_tags).merge(ArticleTag.where(tag_id: tag_id)) }
scope :body_contain, ->(word) { joins(:sentences).merge(where('sentences.body LIKE ?', "%#{word}%")) }
#公開状態でも下書き状態でも検索できるようにするため
sentencesテーブルを結合させて、sentencesテーブルのbodyカラムで検索していると理解しました。

参考文献

tanakanoblogdesu.hatenablog.com

ログイン - はてな