0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

railsチュートリアルまとめ10 ユーザーの更新・表示・削除

Posted at

個人的リマインド用

参考
Ruby on Rails チュートリアル プロダクト開発の0→1を学ぼう

ユーザーの更新・表示・削除

ユーザーを更新する

編集の時はeditアクション。同様に、PATCHリクエストに応答するupdateアクションを作成する。気をつけるのはユーザー情報を更新していいのは本人だけという点。これはbeforeフィルターを使ってなんとかする。

編集フォーム

編集用のページを作成するが、例えばユーザー1のページを作成したいときのURLは/users/1/edit。このユーザーIDはparams[:id]変数で取り出せる。

app/controllers/users_controller.rb

def edit
    @user = User.find(params[:id])
end

ビューファイルは手動で作成する必用がある。
注目点としては、error_messagesパーシャルを再利用しているところと、Gravatarへのリンクでtarget="_blank"が使われている点。これは開く時に別のタブで開いてくれるようにする。
また@userインスタンス変数を使うと、NameやEmailのフィールド値が入った状態でレンダリングされる。

app/views/users/edit.html.erb

<div class="gravatar_edit">
  <%= gravatar_for @user %>
  <a href="https://gravatar.com/emails" target="_blank">change</a>
</div>

これから生成されたHTMLファイルはだいたい予想通り。しかし注目点としては以下の通り。

<input name="_method" type="hidden" value="patch" />

入力フィールドの中に隠し属性があるということ。WebブラウザはRESTの慣習として要求されているPATCHリクエストをそのままでは送信できないので、RailsはPOSTリクエストと隠しinputフィールドを利用して、PATCHリクエストを「偽装」している。

またここで気になる点がある。上のerbのform_with(@user)のコードは、第7章のコードと完全に同じ。では、どのようにそれらを判別しているのか。その答えは、Railsはユーザーが新規なのか、それともデータベースに存在する既存のユーザーなのかを、Active Recordのnew_record?論理値メソッドで区別できるから。

>> User.new.new_record?
=> true
>> User.first.new_record?
=> false

trueの時はPOSTを使い、falseの場合はPATCHを使う。

仕上げに_header.html.erb内にある、Settingsへのリンクを更新する。

<li><%= link_to "Settings", edit_user_path(current_user) %></li>

編集の失敗

まずはupdateアクションの作成から進めるが、これはupdateを使って送信されたparamsハッシュに基づいてユーザーを更新する。

app/controllers/users_controller.rb

def update
    @user = User.find(params[:id])
    if @user.update(user_params) # ←user_paramsを使っている
      # 更新に成功した場合を扱う
    else
      render 'edit', status: :unprocessable_entity
    end
end

user_paramsを使っていることに注目。ここではStrong Parametersを使いマスアサインメントの脆弱性を防止している。
既にUserモデルのバリデーションとエラーメッセージのパーシャルがあるので、無効な情報を送信するとわかりやすいエラーメッセージが表示される。

編集失敗時のテスト

まずは統合テストの作成から。

rails g integration_test users_edit

簡単なテストを追加する。まず編集ページにアクセスし、editビューがレンダリングされているかをチェックする。その後、無効な情報を送信し、editビューが再描画されているかをチェック。

test/integration/users_edit_test.rb

require "test_helper"

class UsersEditTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end

  test "unsuccessful edit" do
    get edit_user_path(@user)
    assert_template 'users/edit'
    patch user_path(@user), params: { user: { name:  "",
                                              email: "foo@invalid",
                                              password:              "foo",
                                              password_confirmation: "bar" } }

    assert_template 'users/edit'
  end
end

結果はgreen。

TDDで編集を成功させる

今度は編集フォームが動作するようにする。まずは実装前に統合テストを書くことから。ちなみにこれは「受け入れテスト」と呼ばれている。

手順としては、ユーザー情報を更新する正しい振る舞いをテストで定義する。次に、フラッシュメッセージが空ではないかどうか、プロフィールページにリダイレクトされるかどうかをチェック。また、データベース内のユーザー情報が正しく変更されたかどうかも検証する。

test/integration/users_edit_test.rb

test "successful edit" do
  get edit_user_path(@user)
  assert_template 'users/edit'
  name  = "Foo Bar"
  email = "foo@bar.com"
  patch user_path(@user), params: { user: { name:  name,
                                            email: email,
                                            password:              "",
                                            password_confirmation: "" } }
  assert_not flash.empty?
  assert_redirected_to @user
  @user.reload
  assert_equal name,  @user.name
  assert_equal email, @user.email
end

パスワードの変更が不要な場合はパスワードを入力せずに更新できると便利。
また@user.reloadメソッドで最新のユーザー情報を反映させ、更新内容が正しいことを確認している。
上のテストを合格させるためにupdateの更新成功の欄を書き換える

def update
  @user = User.find(params[:id])
  if @user.update(user_params)
    flash[:success] = "Profile updated"
    redirect_to @user
  else
    render 'edit', status: :unprocessable_entity
  end
end

しかしまだredのまま。これはパスワードの長さに関するバイデーションを設定しているので、現在の空である状態が引っかかっている。これはvalidatesにallow_nil: trueを追加することで解決できる。

app/models/user.rb

class User < ApplicationRecord
  attr_accessor :remember_token
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }, allow_nil: true
  . # ↑この部分
  .
  .
end

ちなみに空を許したとしても、新規作成の時には存在性を検証するようになっているので、パスワードが空で新規作成されることはない。

これでテストはgreenになる。

認可

Webアプリケーションでは、認証はサイトのユーザーを識別することであり、認可はそのユーザーが実行可能な操作を管理すること。

現在の大きな問題点は、誰でもユーザー情報を編集できること。そこでユーザーにログインを要求し、かつ自分以外のユーザー情報を変更できないように制御する。このようなセキュリティ上の制御機構をセキュリティモデルという。

今回やること
・ログインしていないユーザーが保護されたページにアクセスする時、ログインページに転送し、分かりやすいメッセージを表示する
・ログイン済みのユーザーが、許可されていないページにアクセスしようとしたら、ルートURLにリダイレクトすることにする

ユーザーにログインを要求する

上のような転送の仕組みを使いたい時はbeforeフィルターを使う。これは、なんらかの処理が実行される直前に特定のメソッドを実行する仕組み。

app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :logged_in_user, only: [:edit, :update]
  .
  .
  .
  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end

    # beforeフィルタ

    # ログイン済みユーザーかどうか確認
    def logged_in_user
      unless logged_in?
        flash[:danger] = "Please log in."
        redirect_to login_url, status: :see_other
      end
    end
end

デフォルトでbeforeフィルターは全てのアクションに適用されるので、only:という風に制限する。また、今後どこかの時点で、:destroyアクションを保護するために使われる可能性があるので、logged_in_userの中で、ステータスコード:see_otherを含める。

現段階でテストがredになるが、なぜかというとedit,updateアクションでログインが必須になり、ログインしていないユーザーでこれらのテストが失敗するようになったため。では、テストする前にログインさせるためには、log_in_asヘルパーを使う。

test/integration/users_edit_test.rb

  test "unsuccessful edit" do
    log_in_as(@user)
    get edit_user_path(@user)
    .
    .
    .
  end

  test "successful edit" do
    log_in_as(@user)
    get edit_user_path(@user)
    .
    .
    .
  end

セキュリティモデルに関して、beforeフィルターのところをコメントアウトしてみると、テストがgreenになる。ここはredにならないといけない。

beforeフィルターはアクションごとに適用されているので、Usersコントローラのテストもアクションごとに書く。具体的には、正しい種類のHTTPリクエストを使ってeditアクションとupdateアクションをそれぞれ実行し、flashメッセージが代入されたかどうか、ログイン画面にリダイレクトされたかどうかを確認する。

test/controllers/users_controller_test.rb

require "test_helper"

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user = users(:michael)
  end
  .
  .
  .
  test "should redirect edit when not logged in" do
    get edit_user_path(@user)
    assert_not flash.empty?
    assert_redirected_to login_url
  end

  test "should redirect update when not logged in" do
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert_not flash.empty?
    assert_redirected_to login_url
  end
end

これでテストはredになる。そしてコメントアウト部分を戻したらgreenになる。

正しいユーザーを要求する

ログインを要求するだけではなく、ユーザーが自分の情報だけを編集できる必用がある。
fixtureで別アカウトを用意する。

test/fixtures/users.yml
michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

そして間違ったユーザーファ編集しようとしたときのテストを書く

test/controllers/users_controller_test.rb

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect edit when logged in as wrong user" do
    log_in_as(@other_user)
    get edit_user_path(@user)
    assert flash.empty?
    assert_redirected_to root_url
  end

  test "should redirect update when logged in as wrong user" do
    log_in_as(@other_user)
    patch user_path(@user), params: { user: { name: @user.name,
                                              email: @user.email } }
    assert flash.empty?
    assert_redirected_to root_url
  end

では正しいユーザーかを判別するcorrect_userというメソッドを作成し、beforeフィルターからこのメソッドを呼び出す。

app/controllers/users_controller.rb
  before_action :logged_in_user, only: [:edit, :update]
  before_action :correct_user,   only: [:edit, :update]

  private

    # 正しいユーザーかどうか確認
    def correct_user
      @user = User.find(params[:id])
      redirect_to(root_url, status: :see_other) unless @user == current_user
    end

ちなみにこの時点で、correct_userで@user変数を定義したので、edit,updateアクションから@userへの代入文を削除している。

@user == current_userをcurrent_user(@user)にするリファクタリングは本文を参照。

フレンドリーフォワーディング

残っている問題点としては、保護されたページにユーザーがアクセスしようとしたら、問答無用で自分のプロフィールページに移動させられる。リダイレクト先は直前にユーザーが開こうとしていたページにするのが親切。

まずはテスト。これはログインした後に、デフォルトのプロフィールページではなく編集ページにリダイレクトされるかのテスト。

test/integration/users_edit_test.rb

  test "successful edit with friendly forwarding" do
    get edit_user_path(@user)
    log_in_as(@user)
    assert_redirected_to edit_user_url(@user)

失敗するテストが書けたので、次は実装。ユーザーを適切なページに転送するには、リクエストされたページをどこかに保存しておく。それはSessionヘルパーのstore_location、メソッドにカプセル化しておく。

app/helpers/sessions_helper.rb

module SessionsHelper
  .
  .
  .
  # アクセスしようとしたURLを保存する
  def store_location
    session[:forwarding_url] = request.original_url if request.get?
  end
end

request.original_urlでリクエスト先を取得できる。GETリクエストの時だけにすることで、例えばログインしていないユーザーがフォームを送信した場合は、転送先のURLを保存しないようにできる。

app/controllers/users_controller.rb

# ログイン済みユーザーかどうか確認
def logged_in_user
  unless logged_in?
    store_location
    flash[:danger] = "Please log in."
    redirect_to login_url, status: :see_other
  end
end

フォワーディング自体を実装する時は、リクエストされたURLが存在する場合はそこにリダイレクトし、存在しない場合は何らかのデフォルトURLにリダイレクトするようにする。ログイン成功後にリダイレクトしたいため、この実装はSessionコントローラのcreateアクションに追加する。またreset_sessionの呼び出しがあるので、以下のようにセットする必用がある。

forwarding_url = session[:forwarding_url]
reset_session
log_in user

転送先URLが存在する場合はそこにリダイレクトし、転送先URLがnilの場合はユーザーのプロフィールにリダイレクトできるようにする

redirect_to forwarding_url || url

フレンドリーフォワーディングを備えたcreateアクションは以下の通り

app/controllers/sessions_controller.rb

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      forwarding_url = session[:forwarding_url]
      reset_session
      params[:session][:remember_me] == '1' ? remember(user) : forget(user)
      log_in user
      redirect_to forwarding_url || user
    else
      flash.now[:danger] = 'Invalid email/password combination'
      render 'new', status: :unprocessable_entity
    end
  end

すべてのユーザーを表示する

indexアクションの追加とページネーションの追加

ユーザーの一覧ページ

indexページはログインしたユーザーにしか見せないようにし、未登録ユーザーがデフォルトで表示できるページを制限する。

まずはindexアクションが正しくリダイレクトされるかのテスト

test/controllers/users_controller_test.rb

  test "should redirect index when not logged in" do
    get users_path
    assert_redirected_to login_url
  end

次はbeforeフィルターのlogged_in_userにindexアクションを追加

app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update]
  before_action :correct_user,   only: [:edit, :update]

  def index
  end
end

indexアクションにコードを追加。ユーザーを全表示させたい時はUser.allを使う。

app/controllers/users_controller.rb

  def index
    @users = User.all
  end

それをビューに表示

app/views/users/index.html.erb

<% provide(:title, 'All users') %>
<h1>All users</h1>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

_header.html.erbの名前付きルーティングも忘れずに

サンプルユーザー

見栄え的にユーザーを100人ぐらい増やしたい。そんな時はGemfileにFaker gemを追加する。これはdevelopment環境だけで使うのが普通だが、本文では本番環境でも使っている。

Gemfile
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

gem "rails",           "7.0.4"
gem "bcrypt",          "3.1.16"
gem "faker",           "2.21.0"
gem "bootstrap-sass",  "3.4.1"

bundle

サンプルユーザーを生成するスクリプトを追加する。ファイルとしてはdb/seeds.rbを使う。

db/seeds.rb
# メインのサンプルユーザーを1人作成する
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar")

# 追加のユーザーをまとめて生成する
99.times do |n|
  name  = Faker::Name.name
  email = "example-#{n+1}@railstutorial.org"
  password = "password"
  User.create!(name:  name,
               email: email,
               password:              password,
               password_confirmation: password)
end

create!は基本的にcreateメソッドと同じだが、ユーザーが無効な場合にfalseを返すのではなく、例外を発生させる。

rails db:migrate:reset
rails db:seed

ページネーション

Gemfileにwill_paginate gemとbootstrap-will_paginate gemを追加し、Bootstrapのページネーションスタイルを使ってwill_paginateを構成する。

Gemfile

source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

gem "rails",                   "7.0.4"
gem "bcrypt",                  "3.1.16"
gem "faker",                   "2.21.0"
gem "will_paginate",           "3.3.1"
gem "bootstrap-will_paginate", "1.0.0"

bundle

今回は画面の上下にページネーションを配置

app/views/users/index.html.erb

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <li>
      <%= gravatar_for user, size: 50 %>
      <%= link_to user.name, user %>
    </li>
  <% end %>
</ul>

<%= will_paginate %>

will_paginateメソッドは、usersビューのコードの中から、@usersオブジェクトを自動的に見つけ出し、それから他のページにアクセスするためのページネーションリンクを作成している。

しかし、このままではページネーションは動かない。indexアクション内のUser.allを書き換える必用がある。

app/controllers/users_controller.rb

  def index
    @users = User.paginate(page: params[:page])
  end

ユーザー一覧のテスト

今回のテストは「ログイン」「indexページにアクセス」「最初のページにユーザーがいることを確認」「ページネーションのリンクがあることを確認」という順序で行う。少なくともユーザーが31人必用。fixtureでもありがたいことにERBが使えるので、これを使って対応する。

test/fixtures/users.yml

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>

archer:
  name: Sterling Archer
  email: duchess@example.gov
  password_digest: <%= User.digest('password') %>

lana:
  name: Lana Kane
  email: hands@example.gov
  password_digest: <%= User.digest('password') %>

malory:
  name: Malory Archer
  email: boss@example.gov
  password_digest: <%= User.digest('password') %>

<% 30.times do |n| %>
user_<%= n %>:
  name:  <%= "User #{n}" %>
  email: <%= "user-#{n}@example.com" %>
  password_digest: <%= User.digest('password') %>
<% end %>

ユーザーができたので統合テストファイルを作る

rails g integration_test users_index
test/integration/users_index_test.rb

  test "index including pagination" do
    log_in_as(@user)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    User.paginate(page: 1).each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
    end
  end

paginationクラスを持つdivタグをチェックすることで、ユーザー一覧の最初のページが存在することを確認する。

パーシャルページのリファクタリング

リファクタリングの第一歩はliをrender呼び出しに書き換える。

app/views/users/index.html.erb

<%= will_paginate %>

<ul class="users">
  <% @users.each do |user| %>
    <%= render user %>
  <% end %>
</ul>

<%= will_paginate %>

renderをパーシャルに対してではなく、Userクラスのuser変数に対して実行していることに注目。ここでは、自動的に_user.html.erbという名前のパーシャルを探索するので、このパーシャルを作成する必用がある。

app/views/users/_user.html.erb

<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
</li>

さらに改善して、今度はrenderを@users変数に対して直接呼び出す。

app/views/users/index.html.erb

<ul class="users">
  <% @users.each do |user| %>
    <%= render @users %>
  <% end %>
</ul>

Railsは@usersをUserオブジェクトのリストであると推測する。renderにユーザーのコレクションを渡して呼び出すと、Railsは自動的にユーザーのコレクションを列挙し、それぞれのユーザーを_user.html.erbパーシャルで出力する。

ユーザーを削除する

まずは、削除を実行できる権限を持つ管理ユーザーのクラスを作成する。承認においては、このような特権のセットをロールという。

管理ユーザー

論理値型のadmin属性をUserモデルに追加する。

rails g migration add_admin_to_users admin:boolean
db/migrate/[timestamp]_add_admin_to_users.rb

class AddAdminToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :admin, :boolean, default: false
  end
end

default: falseに関しては、デフォルトでは管理者になれないということを明示している。

rails db:migrate

では仕上げに、最初のユーザーだけをデフォルトで管理者にするようサンプルデータを更新する。

db/seeds.rb

# メインのサンプルユーザーを1人作成する
User.create!(name:  "Example User",
             email: "example@railstutorial.org",
             password:              "foobar",
             password_confirmation: "foobar",
             admin: true)
rails db:migrate:reset
rails db:migrate

Strong Parameters
このadmin属性は非常に慎重に扱うべきもので、もしも一般ユーザーからtrueに変えるよう操作されたらとんでもないことになる。なのでStrong Parametersを使う。

def user_params
  params.require(:user).permit(:name, :email, :password,
                               :password_confirmation)
end

上のコードでは、許可された属性リストにadminが含まれていない。これにより、任意のユーザーが自分自身にアプリケーションの管理者権限を与えることを防止できる。

destoroyアクション

現在のユーザーが管理者の時に限り[delete]リンクが表示されるようにする。

app/views/users/_user.html.erb

<li>
  <%= gravatar_for user, size: 50 %>
  <%= link_to user.name, user %>
  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, data: { "turbo-method": :delete,
                                          turbo_confirm: "You sure?" } %>
  <% end %>
</li>

"turbo-method": :deleteは、リンクに必用なDELETEリクエストを発効する準備。
2つ目のturbo_confirm: "You sure?"はJSのconfirmボックスを表示する。

ブラウザはネイティブではDELETEリクエストを送信できないので、RailsではJSを使って偽造する。JSが使えない場合は、フォームとPOSTリクエストを使って偽造できる。

ではdestroyアクションの作成と、beforeフィルターによるログインの要求を行う。

app/controllers/users_controller.rb

  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  .
  .
  .
  def destroy
    User.find(params[:id]).destroy
    flash[:success] = "User deleted"
    redirect_to users_url, status: :see_other
  end

これで管理者だけが削除できるようになったが、まだ大きな穴がある。それは、コマンドラインでDELETEリクエストを直接発効するという方法でサイトの全ユーザーを削除できること。サイトを適切に防衛するには、destroyアクションにもアクセス制御を行う必用がある。

app/controllers/users_controller.rb

class UsersController < ApplicationController
  before_action :logged_in_user, only: [:index, :edit, :update, :destroy]
  before_action :correct_user,   only: [:edit, :update]
  before_action :admin_user,     only: :destroy
  .
  .
  .
  private
    .
    .
    .
    # 管理者かどうか確認
    def admin_user
      redirect_to(root_url, status: :see_other) unless current_user.admin?
    end
end

ユーザー削除のテスト

サンプルユーザーの1人を管理者に変える。

test/fixtures/users.yml

michael:
  name: Michael Example
  email: michael@example.com
  password_digest: <%= User.digest('password') %>
  admin: true

テストでは2つのケースをチェックする。1つは、ユーザーがログインしていない場合は、ログイン画面にリダイレクトされること。もう1つは、ログイン済みであっても管理者でなければ、ホーム画面にリダイレクトされること。

test/controllers/users_controller_test.rb

require "test_helper"

class UsersControllerTest < ActionDispatch::IntegrationTest

  def setup
    @user       = users(:michael)
    @other_user = users(:archer)
  end
  .
  .
  .
  test "should redirect destroy when not logged in" do
    assert_no_difference 'User.count' do
      delete user_path(@user)
    end
    assert_response :see_other
    assert_redirected_to login_url
  end

  test "should redirect destroy when logged in as a non-admin" do
    log_in_as(@other_user)
    assert_no_difference 'User.count' do
      delete user_path(@user)
    end
    assert_response :see_other
    assert_redirected_to root_url
  end
end

destroyアクションに直接DELETEリクエストを発効するためにdeleteメソッドを使う。また、assert_no_differenceを使って、ユーザー数に変化がないことを確認している。

では管理者の振る舞いに関してもテストをする。

test/integration/users_index_test.rb

require "test_helper"

class UsersIndexTest < ActionDispatch::IntegrationTest

  def setup
    @admin     = users(:michael)
    @non_admin = users(:archer)
  end

  test "index as admin including pagination and delete links" do
    log_in_as(@admin)
    get users_path
    assert_template 'users/index'
    assert_select 'div.pagination'
    first_page_of_users = User.paginate(page: 1)
    first_page_of_users.each do |user|
      assert_select 'a[href=?]', user_path(user), text: user.name
      unless user == @admin
        assert_select 'a[href=?]', user_path(user), text: 'delete'
      end
    end
    assert_difference 'User.count', -1 do
      delete user_path(@non_admin)
      assert_response :see_other
      assert_redirected_to users_url
    end
  end

  test "index as non-admin" do
    log_in_as(@non_admin)
    get users_path
    assert_select 'a', text: 'delete', count: 0
  end
end

各ユーザーの削除リンクをテストする時、削除対象のユーザーが管理者の場合はテストをスキップしている。これにより管理者自身には削除リンクが表示されないようになっている。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?