Мультиблог на Ruby on Rails. Создание новой статьи пользователем. Урок 8.

Теперь, когда мы реализовали страницы для регистрации и авторизации пользователя, пришло время заняться тем, чтобы предоставить пользователю возможность создавать новые посты. Итак, у нас есть две сущности - "Посты" и "Пользователи"; наша задача заключается в том, чтобы связать одну сущность с другой. Самый простой способ сделать это - создать у постов поле user_id, которое будет указывать на принадлежность поста к пользователю, который её создал. Другими словами, это поле будет внешним ключом, который будет ссылаться на модель User.

Подготовительный этап перед добавлением поля

Добавить колонку не проблема, но в связи с тем, что у нас уже есть некоторые посты в таблице posts, мы окажемся в той ситуации, когда у всех существующих на данный момент записей, значение user_id будет равно null; что недопустимо, так как какая-то привязка всё-таки должна быть. Поэтому запускаем консоль через rails console и удаляем все посты через команду: 

Post.destroy_all

Добавление поля user_id

В нашем случае необходимо создать колонку user_id в таблице posts - для этого воспользуемся преимуществом фреймворка в именовании миграций, и пропишем команду на создание миграции следующим образом:

rails generate migration AddUserRefToPosts user:references

Применяем миграцию.

Создание связи в модели

Для того, чтобы была возможность создавать запись и привязывать id пользователя к ней, необходимо прописать связь для модели User:

has_many :post

Создание маршрута (с помощью resources)

В Ruby on Rails изначально есть два пути для прописывания маршрутов: 

  1. Для каждого действия определять свой маршрут. В случае с постами это: создание, редактирование, удаление и так далее. 
  2. Использовать уникальную конструкцию resources, которая уже будет включать в себя весь набор соответствий маршрутов и контролеров для обслуживания HTTP-запросов. Но чтобы это всё работало, нам нужно прописать данную конструкцию в файл роутинга и использовать заранее определённые имена action. 

Вот вторым способом мы и воспользуемся, потому что он как раз подходит для наших целей. Сперва пропишем конструкцию в файл роутинга config/routes.rb:

resources :posts

Теперь всё что нам нужно, это в контролере posts использовать соответствующие методы. 

Создание контролера и view для создания нового поста

Метод, который будет отвечать за отображения формы создания поста, должен называться new:

def new
  @post = Post.new
end

Теперь займёмся шаблоном с формой, за это отвечает файл app/views/posts/new.html.erb

<% content_for :h1 do %>
Создание новой статьи
<% end %>

<% content_for :title do %>
Создание новой статьи
<% end %>

<%= render 'form', post: @post %>

Вместо того, чтобы размещать весь код формы, я поместил его в частичное представление form. Сделано так было в рациональных целях, поскольку шаблоны создания и редактирования страницы будут во многом похожи друг на друга. А вот кстати и сама форма:

<%= form_with model: @post do |form| %>

<div class="form-group">
    <%= form.label :title, 'Заголовок статьи' %>
    <%= form.text_field :title, class: 'form-control' %>
</div>

<div class="form-group">
    <%= form.label :body, 'Текст статьи' %>
    <%= form.text_area :body, rows: 5, class: 'form-control' %>
</div>

<%= form.submit 'Сохранить', class: 'btn btn-primary' %>

<% end %>

В соответствии с маршрутом, который уже был прописан ранее, адрес для создания статьи у нас будет: http://127.0.0.1:3000/posts/new 

Но всё что мы сделали, это лишь реализовали action для отображения формы, а теперь нужно написать метод, который бы создавал статью на основе данных из формы: 

def create
  post = current_user.post.create(post_params)
  flash[:notice] = "Статья была создана"
  redirect_to posts_path
end

Здесь переменная current_user содержит текущего пользователя, а метод post (который появился за счёт связи has_many :post) создаёт объект Post, который уже содержит в себе связь с пользователем. Далее с помощью метода create создаётся объект на основе тех данных, которые лежат в post_params. И вот на этом следует остановиться чуть подробнее; откуда берутся эти данные? А за это у нас отвечает приватный метод post_params, который отфильтровывает данные, которые были получены из формы. В нём же указано, какие данные принято считать разрешёнными и каким именно методом они будут переданы: 

private

def post_params
  params.require(:post).permit(:title, :body)
end

Тут может возникнуть вопрос: зачем так заморачиваться? Сделано это в целях безопасности, чтобы пользователь не смог отредактировать форму и передать посредством неё через поле user_id идентификатор другого пользователя; таким образом дискредитируя его. Но мы всё сделали по уму и у нас никто id пользователя через форму поменять не сможет, поскольку поле user_id будет браться непосредственно из объекта пользователя, под которым залогинен пользователь.

Идём дальше по коду:

flash[:notice] - место для этой временной переменной мы чуть ранее определяли в базовом шаблоне. Она используется для однократного отображения каких-то информационных сообщений, которые нужно отобразить на следующей странице, после перенаправления с текущей. 
redirect_to posts_path - перенаправление на страничку со списком всех постов.

Собственно, этого уже достаточно для того, чтобы статья была создана. Но сработает это только в том случае, если вы зарегистрированы; иначе в переменной current_user окажется nil, у которого метода post не окажется и фреймворк выдаст ошибку. Давайте исправим это: сделаем так, чтобы анонимного пользователя, который попытается создать статью, перекидывало на страницу авторизации. 

Запрет гостям доступа к определённому контролеру

Итак, мы уже договорились что в мультиблоге каждая статья должна принадлежать какому-то пользователю. Соответственно нам необходимо запретить анонимным пользователям доступ к контролеру, который отвечает за вывод формы создания поста. Для этих целей отлично подойдёт метод before_action, который используется в том случае, когда нужно выполнить какую-то функцию до того, как будут выполнены определённые методы: 

before_action :authenticate_user!, only: [:new]

В нём мы передаём первым параметром функцию, которая будет срабатывать перед обращением к action, а вторым параметром непосредственно список самх action, для которых будет осуществляться проверка на авторизацию. Есть и другой метод skip_before_action, который работает с точностью до наоборот, так как в нём вторым параметром указывается только список тех action, в которых функция срабатывать не будет. По идее рекомендуется использовать его, потому что он запрещает всё, что не разрешено; и таким образом ещё больше исключает человеческий фактор. Но у нас пока что кода немного, поэтому я использовал before_action

А вот собственно и функция authenticate_user, я прописал её чуть ниже private:

def authenticate_user
  redirect_to new_user_session_path if current_user.nil?
end

данный код означает: если пользователь не авторизован, необходимо перенаправить его на страницу авторизации.

Вывод данных автора статьи

Если вы обратили внимание, то у нас на страничке с постами есть ссылка, в качестве текста для которой, по идее должен отображаться логин автора статьи; но поскольку у нашего пользователя есть только email, его и выведем. Однако, чтобы у нас была возможность получать через объект поста доступ к связанному объекту User, в модели Post должна быть определена связь:

belongs_to :user

После чего отредактируем частичный шаблон _post.html.erb и поменяем там строчку:

<a href="#">Start Bootstrap</a>

на

<a href="#"><%= post.user.email %></a>

Изменения этого урока можно посмотреть в коммите https://github.com/maclen2007/simple_ruby_blog/commit/d95eeec0b5651ecd72097a538c1c4238b443012c