Мультиблог на 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 изначально есть два пути для прописывания маршрутов:
- Для каждого действия определять свой маршрут. В случае с постами это: создание, редактирование, удаление и так далее.
- Использовать уникальную конструкцию 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