whenever による記事数一覧のメール送信
実装したいこと
・wheneverを導入して毎日am9:00に下記の内容を管理者にメールで送信させるメールの件名には「公開済記事の集計結果」と設定
・管理者のメールアドレスにはadmin@example.comを設定
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) - おぴよの気まぐれ日記
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メソッド
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を使用していないと悪意ある第三者に情報が漏れてしまう危険性が高まります。
HTTPとHTTPSの違い
HTTPとHTTPSの主な違いは通信内容が暗号化されていないか、されているかの違い
Google ChromeではHTTPSに対応したホームページを開くとブラウザのURLの左の方に「保護された通信」と表示、また「保護された通信」ではなく、組織名が表示される場合もある。
ステートフルとステートレス
ステートフル
直前にやりとりした相手などの状態を、以降のやりとりでも覚えていること。
(例) ショッピングサイトの買い物かご、データを一時保存
ステートレス
毎回リセットするものをステートレス(前回の状態を覚えていない)
Cookie クッキー
クッキー(Cookie)とは、Webサイトの訪問者の情報を一時的に保存するための仕組みであり、それぞれのユーザーの識別を目的としている。
具体的には、ユーザーの購買履歴や、利用している地域などがわかる会員証のようなものとよく例えられます。2回目以降同じサイトに訪れる際は、初めてレスポンスを受けとる際に付与されたクッキー(会員証)を元にサーバーがブラウザを特定します。
クッキー(Cookie)はパソコンやスマートフォンに保存されますが、ファイルサイズが小さいため空き容量を減らす原因にはなりません。
セッション
クライアントとサーバーで通信を行う場合であれば、クライアントからサーバーへ接続した時点でセッションが始まり、サーバーから切断するとセッションが終了します。この一連の流れを管理することをセッション管理という
(例) 商品を選ぶ→かごに入れる→購入の流れをセッションと呼ぶ
参考文献
【Web技術の基本】知っているつもりの専門用語まとめ - Qiita
トップ画像をスライダー形式に変更
ActiveStorageとは
Amazon S3、Google 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と適切に切り替わるようにする
・ブロック追加画面のアイコンはYoutubeとTwitterの二つが表示されるようにする
イメージ
カラム準備
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
最初はgoogleがyoutube投稿された全ての動画の全てに割り振った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
参考文献
アイキャッチの表示サイズ / 位置指定
実装したいこと
・アイキャッチ画像をユーザーの入力した幅に設定したい。
・アイキャッチ画像をユーザーの選択した位置に設定したい。
アイキャッチとは
投稿ページの記事タイトルの下に表示される画像のこと
画像の表示サイズの設定
カラム追加
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: '右寄せ'
参考文献
アクション権限の調整
実装したこと
・記事投稿アプリで、管理者や編集者以外は記事の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」みたいにエラー用の画面が必要です。
参考文献
検索機能の追加
実装したいこと
・著者、タグ、コンテンツ(記事内容)に関しても検索が行えるようにする
・著者、タグはセレクトボックスによる選択、コンテンツ(記事内容)はフリーワード検索が行えるようにする
・追加する各検索機能では、下書き状態の記事も検索できるように実装
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カラムで検索していると理解しました。
参考文献