
Зачем и для кого эта статья?
Всем привет! Я Ruby on Rails Developer и еще совсем недавно я начинал свой путь в этой области. Я уже прошел первые шаги (о них я писал в данной статье), как выбор языка, изучение его основ, знакомство с фреймворком, первые pet-проекты, первые собеседования, первый оффер, первая компания. Но многие только начали идти по этому пути и именно для них эта статья. По своему опыту помню, как сложно искать гайды (большинство из них про создание книжных магазинов, личных блогов и т.д.), поэтому, надеюсь, многим понравиться идея создания соц сети.
Почему соц сеть и как будет идти процесс?
Во-первых, мне самому было бы интересно реализация данного проекта. Во-вторых, я вдохновился книгой Practical Rails Social Networking Sites by Alan Bradburne. Можно было бы сделать все по книге, скажете вы. Но зачем тогда я и мои статьи? Книга 2007 года и версия ruby там 1.8, поэтому решения в большинстве своем будут неактуальны в наше время. К тому же, я не собираюсь делать все по книге, а лишь руководствоваться ей (дизайн буду использовать из нее с добавлением bootsrap). Во время разработки я буду использовать многие гемы, которые будут полезны для начинающих разработчиков. Но скажу сразу: эта серия статей не для самого старта. Некоторые базовые вещи (установка ruby, rails, что такое MVC, git и тому подобное) я буду пропускать, поэтому рекомендую отложить данный цикл статей и вернуться к нему чуть позже. Если вы опытный разработчик и читаете эту статью, буду очень признателен услышать ваше мнение. Касательно периодичности выхода статей и сколько именно их будет не могу сказать, так как планирую делать проект в свободное время от работы и параллельно писать статьи. Но буду стараться не откладывать в долгий ящик и делать все с хорошим темпом.
Создаем проект и делаем первичные настройки
Перед тем, как мы начнем, установите следующие версии:
-
Ruby 3.0.3 -
Rails 6.1.4.6 -
MySQL 8.0 -
Node 10.19 -
Yarn 1.22.17
Очень советую вам использовать github во время разработки данного проекта. Чуть позже мы с вами настроим CI/CD на нашем проекте, что будет крайне полезным для вас опытом. Теперь создадим наш проект. Я назову его g_connect, но вы можете использовать любое другое название (если выберите другое, везде, где я буду использовать g_connect, пишите свое).
rails new g_connect -d mysql
Теперь переходим в папку с проектом и займемся первичными настройками. Я всегда начинаю с Gemfile и некоторые гемы, которые точно буду использовать во время разработки.
#Gemfile source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '3.0.3' gem 'aasm' gem 'bootsnap', '>= 1.4.4', require: false gem 'bootstrap' gem 'devise' gem 'jbuilder', '~> 2.7' gem 'mysql2', '~> 0.5.2' gem 'puma', '~> 5.0' gem 'rails', '~> 6.1.4.6' gem 'sass-rails', '>= 6' gem 'slim' gem 'turbolinks', '~> 5' gem 'webpacker', '~> 5.0' group :development, :test do gem 'better_errors' gem 'binding_of_caller' gem 'byebug', platforms: %i[mri mingw x64_mingw] gem 'factory_bot_rails' gem 'faker' gem 'rails-controller-testing' gem 'rspec-rails' gem 'rubocop' gem 'rubocop-rails' gem 'rubocop-rspec' end group :development do gem 'annotate' gem 'listen', '~> 3.3' gem 'rack-mini-profiler', '~> 2.0' gem 'spring' gem 'web-console', '>= 4.1.0' end group :test do gem 'capybara', '>= 3.26' gem 'rspec_junit_formatter' gem 'selenium-webdriver' gem 'simplecov', require: false gem 'webdrivers' end gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
Давайте я поясню, для чего некоторые из этих гемов (более подробно рекомендую перед стартом ознакомиться с документацией):
-
gem ‘aasm’ — его мы будем использовать для транзакций между состояниями
-
gem ‘bootstrap’ — это нам нужно будет для нашего дизайна (я делаю упор на бэк и совсем немного буду уделять время фронту, но в самом конце может и придет вдохновение на наведение красоты)
-
gem ‘devise’ — аутентификация (коротко и ясно, будет еще гем для авторизации, но я еще не выбрал, какой)
-
gem ‘slim’ — сэкономим время на написании html-тегов
-
gem ‘better_errors’ — красивый вывод ошибок в браузере (например, роут указали неправильный)
-
gem ‘factory_bot_rails’ — шаблон, который будем использовать в наших тестах
-
gem ‘faker’ — для создания фейковых данных
-
gem ‘rspec-rails — это подключаем среду тестирования
RSpecдля нашего проекта -
gem ‘rubocop’ — проверяем, насколько хорошо написан наш код
-
gem ‘annotate’ — для автоматической аннотации наших моделей (зачем переключаться между моделей и миграцией, если есть этот гем)
-
gem ‘simplecov’ — для просмотра, все ли мы покрыли тестами
Теперь нужно установить все гемы и их зависимости, поэтому запускаем bundle в нашем терминале (кстати да, забыл сказать, что использую Ubuntu, поэтому для MacOS/Windows(вот винду вообще не трогайте лучше при разработке на ruby, но уж если оч хочется) смотрите некоторые моменты самостоятельно). Также можем удалить папку test, она не понадобиться (ведь будем писать rspec-и).
После этого настроим нашу БД. В файле config/database.yml укажите свои username и password (я стандартно делал root/root). После этого запускаем следующее (если кто-то не знает, то эта одна команда сразу же выполняет db:create, db:schema:load и db:seed):
rails db:setup
Некоторые из наших гемов требуют дополнительной настройки. Ими мы сейчас и займемся (devise также требует дополнительной настройки, но им мы займемся позже, когда будем делать аутентификацию). Начнем с bootstrap. Переходим в файл app/assets/stylesheets/application.scss (файл может вначале иметь расширение .css, поэтому исправьте его на .scss) и добавляем следующую строчку:
/*app/assets/stylesheets/application.scss*/ @import "bootstrap";
Теперь настроим annotate. Для этого в терминале запустите следующую команду:
rails g annotate:install
Теперь нужно настроить rspec:
rails g rspec:install
Также для наших тестов нам понадобится настройка factory_bot_rails и simplecov. В нашей папке spec создаем папку support, а в ней создаем файл factory_bot.rb со следующим кодом:
#spec/support/factory_bot.rb RSpec.configure do |config| config.include FactoryBot::Syntax::Methods end
Теперь переходим в наш spec/rails_helper.rb. Здесь мы подключим наш файл для factory_bot, а так же подключим simplecov (две строчки для подключения simplecov должны быть в самом начале файла).
#spec/rails_helper.rb require 'simplecov' SimpleCov.start 'rails' require_relative './support/factory_bot'
С настройкой гемов пока что закончили. Если вы хотите проверить процент покрытия тестами, то можете запустить xdg-open coverage/index.html и вот она магия. Но я бы хотел добавить еще пару моментов. Первый — shared_context.rb для тестов наших моделей. В папке spec/support создайте shared_context.rb
#spec/support/shared_context.rb RSpec.shared_examples 'creates_object_for' do |model_name| subject { FactoryBot.build(model_name) } it 'creates object' do expect { subject.save }.to change { described_class.count }.by(1) end end RSpec.shared_examples 'not_create_object_for' do |model_name, parameter| subject { described_class.create(attributes) } let(:attributes) { FactoryBot.attributes_for(model_name, parameter) } it 'does not create object' do expect { subject.save }.to change { described_class.count }.by(0) end it 'raise RecordInvalid error' do expect { subject.save! }.to raise_error(ActiveRecord::RecordInvalid) end end
Наш shared_context будет играть роль шаблона для тестирования моделей. В нем мы опишем, что должно происходить, если данные валидны и объект создаётся, и наоборот, что будет происходить, если какие-то данные невалидны либо отсутствуют и объект не будет создаваться. Это заметно упростит написание тестов для моделей, потом на практике вы в этом убедитесь. Теперь давайте подключим его в наш spec_helper.rb
#spec/spec_helper.rb require_relative './support/shared_context'
И последнее по настройкам перед стартом: чутка дизайна. Перейдите в app/views/layouts/application.html.erb. Измените расширение .erb на .slim и сделайте вот так:
#app/views/layouts/application.html.slim doctype html html head meta content='text/html; charset=UTF-8' http-equiv='Content-Type' title G-Connect = csrf_meta_tags = csp_meta_tag = stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' = javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' body #container #header #sidemenu = render 'application/sidemenu' #content = yield
Как по мне, это намного лучше смотрится, чем .html.erb. Если вы до этого никогда не использовали .slim, то используйте вот этот ресурс для перевода из .html.erb в .html.slim. Далее в папке app/views создаем папку application, а в ней файл _sidemenu.html.slim и внутри него пока что только следующие строчки:
#app/views/layouts/_sidemenu.html.slim ul li = link_to 'Home', '/', class: 'btn btn-sm btn-light'
Затем переходим в app/assets/stylesheets/application.scss и добавляем следующее:
/*app/assets/stylesheets/application.scss*/ body { margin: 0; padding: 0; background-color: #f0ffff; font-family: Arial, Helvetica, sans-serif; } #header { background-color: #f0ffff; height: 60px; margin-top: 10px; text-align: left; padding-top: 1px; } #container { width: 760px; min-width: 760px; margin: 0 auto; padding: 0px; } #sidemenu { font-size: 80%; float: left; width: 100px; padding: 0px; } #sidemenu ul { list-style: none; margin-left: 0px; padding: 0px; } a { color: #b00; } a:hover { background-color: #b00; color: #f0ffff; } #content { float: right; width: 650px; } th { background-color: #933; color: #f0ffff; } tr.odd { background-color: #fcc; } tr.even { background-color: #ecc; }
Пока совсем простенько (css взят из книги), но, как я и говорил ранее, я делаю упор на бэк. Когда первичная работа окончена, можем двигаться дальше. Может залить все, что мы с вами сделали, на github и делать новые вещи уже в другой ветке. Рекомендую перед этим сделать проверку rubocop-ом (можете добавить специальное расширение для вашей IDE и он сразу будет подсвечивать вам файлы, в которых есть недочеты).
Создание модели страниц
Первую модель мы сделаем для страниц. У нее будут следующие поля
|
Имя поля |
Тип |
|
|
|
|
|
|
|
|
|
|
|
|
Permalink будет составляться из нашего title, только будет иметь более красивый вид, чтобы использовать его в качестве url. Давайте создадим модель.
rails g model Page
Помимо модели у нас сгенерировалась миграция и два файла для тестов, к ним мы вернемся чуть позже. Миграция находится в папке db/migrate. Давайте начнем с нее:
#db/migrate/date_time_create_pages.rb class CreatePages < ActiveRecord::Migration[6.1] def change create_table :pages do |t| t.string :title, null: false t.string :permalink t.text :body, null: false end end end
После этого запускаем rails db:migrate (наш annotate сразу же показывает, какие файлы были аннотированы) и переходим в нашу модель. Здесь мы пропишем наши валидации, а так же метод для получения нашего permalink (его мы будем вызывать с помощью коллбэка after_create). В этом методе я буду использовать регулярные выражения, для помощи с их составлением я всегда использую Rubular.
#app/model/page.rb class Page < ApplicationRecord after_create :clean_url validates_presence_of :title, :body validates :title, length: { in: 3..250 } validates :body, length: { in: 3..100_00 } private def clean_url return unless self.permalink.nil? url = title.downcase.gsub(/\s+/, '_').gsub(/[^a-zA-Z0-9_]+/, '') self.permalink = url save end end
Теперь предлагаю заняться нашими тестами. Начнем с файла spec/factories/pages.rb
#spec/factories/pages.rb FactoryBot.define do factory :page do title { 'Test' } body { 'Test' } end end
После этого можем заняться непосредственно написанием тестов. Именно в этом файле нам и пригодится наш написанный ранее spec/supprot/shared_context.rb.
#spec/models/page_spec.rb require 'rails_helper' RSpec.describe Page, type: :model do describe '.create' do context 'with valid attributes' do include_examples 'creates_object_for', :page end context 'with invalid attributes' do context 'with short title' do include_examples 'not_create_object_for', :page, title: 'te' end context 'with too long title' do include_examples 'not_create_object_for', :page, title: Faker::String.random(length: 253) end context 'with short body' do include_examples 'not_create_object_for', :page, body: 'te' end context 'with too long body' do include_examples 'not_create_object_for', :page, body: Faker::String.random(length: 100_02) end end context 'with missing attributes' do context 'with missing title' do include_examples 'not_create_object_for', :page, title: nil end context 'with missing body' do include_examples 'not_create_object_for', :page, body: nil end end end end
Думаю, здесь не нужно никаких пояснений, лишь отмечу, что private методы не нуждаются в тестировании, поэтому здесь и нет тестов для нашего clean_url. Можем запустить rspec в нашем терминале и убедиться, что все наши тесты проходят без ошибок. Когда с моделью и тестами для них мы разобрались, предлагаю заняться контроллером. Я не использую генератор для контроллеров, поэтому создаем файл pages_controller.rb в нашей папке app/controllers. Здесь мы пропишем следующее:
#app/controllers/pages_controller.rb class PagesController < ApplicationController before_action :find_page, only: %i[show edit update destroy] def index @pages = Page.all end def show; end def new @page = Page.new end def create @page = Page.create(page_params) if @page.save redirect_to pages_path, notice: 'Page created' else render :new end end def edit; end def update if @page.update(page_params) redirect_to page_path(@page), notice: 'Page updated' else render :edit end end def destroy @page.destroy redirect_to pages_path, notice: 'Page deleted' end private def find_page @page = Page.find(params[:id]) end def page_params params.require(:page).permit(:title, :permalink, :body) end end
Здесь все с больше абсолютно стандартное, поэтому также не буду заострять на этом внимание. Теперь нужно добавить роуты для нашего контроллера:
#config/routes.rb Rails.application.routes.draw do root 'pages#index' resources :pages end
Теперь давайте покроим тестами наш контроллер. Для это в папке spec создаем папку controllers и там же сразу создаем файл pages_controller_spec.rb
#spec/controllers/pages_controller_spec.rb require 'rails_helper' RSpec.describe PagesController, type: :controller do describe 'GET #index' do let(:pages) { [FactoryBot.create(:page)] } it 'returns all pages' do get :index expect(response).to render_template('index') expect(response).to have_http_status(:ok) expect(assigns(:pages)).to eq(pages) end end describe 'GET #show' do let(:page) { FactoryBot.create(:page) } it 'assigns page' do get :show, params: { id: page.id } expect(response).to render_template('show') expect(response).to have_http_status(:ok) expect(assigns(:page)).to eq(page) end end describe 'GET #new' do it 'returns render form for creating new page' do get :new expect(response).to render_template('new') expect(response).to have_http_status(:success) end end describe 'POST #create' do let(:page_params) { FactoryBot.attributes_for(:page) } it 'creates new page' do get :create, params: { page: page_params } expect(response).to redirect_to('/pages') expect(response).to have_http_status(:found) end it 'doesn`t create new page' do get :create, params: { page: page_params.except(:title) } expect(response).to render_template('new') end end describe 'PUT #update' do let(:page) { FactoryBot.create(:page) } it 'updates the requested page' do put :update, params: { id: page.id, page: { title: 'brbrbr' } } expect(response).to redirect_to("/pages/#{page.id}") expect(response).to have_http_status(:found) end it 'doesn`t update page' do put :update, params: { id: page.id, page: { title: '' } } expect(response).to render_template('edit') end end describe 'DELETE #destroy' do let(:page) { FactoryBot.create(:page) } it 'destroys page' do delete :destroy, params: { id: page.id } expect(response).to redirect_to('/pages') end end end
Теперь же предлагаю сделать визуал для нашего контроллера. Переходим в app/views и создаем там папку pages, а в ней 5 файлов: _form.htm.slim, new.html.slim, edit.html.slim, show.html.slim и index.html.slim. Теперь пройдемся по каждому из них. В нашем _form.htm.slim будет находиться форма, которую мы будем заполнять для создания или изменения наших Pages. Эту форму мы будем рендерить в наших new и edit соответственно.
#app/views/pages/_form.html.slim = form_with(model: page, local: true) do |f| .form-group = f.label :title = f.text_field :title .form-group = f.label :body = f.text_area :body .form-group = f.submit 'Submit', class: 'btn btn-success'
#app/views/pages/new.html.slim h1 New Page = render 'form', page: @page = link_to 'Back', :back, class: 'btn btn-sm btn-primary'
#app/views/pages/edit.html.slim h1 Edit Page = render 'form', page: @page = link_to 'Back', :back, class: 'btn btn-sm btn-primary'
Теперь займемся show и index:
#app/views/pages/show.html.slim p strong Title: = @page.title p strong Body: = @page.body = link_to 'Edit', edit_page_path(@page), class: 'btn btn-sm btn-success' ' = link_to 'Delete', page_path(@page), method: :delete, class: 'btn btn-sm btn-danger', data: { confirm: 'Are you sure?' } ' = link_to 'Back', :back, class: 'btn btn-sm btn-primary'
#app/views/pages/index.html.slim h2 Pages ul - @pages.each do |page| li = page.permalink |: = page.title | p = link_to 'Show', page_path(page), class: 'btn btn-sm btn-info' p = link_to 'Create new page', new_page_url, class: 'btn btn-sm btn-primary'
И последняя вещь на сегодня — немного подправим _sidemenu.html.slim
#app/views/application/_sidemenu.html.slim ul li = link_to 'Home', root_path, class: 'btn btn-sm btn-light'
Думаю, на сегодня уже достаточно. Итак довольно объемная статья получилась. Проверьте все rubocop-ом, исправьте недочеты, если нужно, и можете смело заливать на ваш github. В следующей статье мы добавим пользователей, devise и настроим CI/CD. Надеюсь вам всем было интересно читать эту статью. Если есть какие-то замечания либо же предложения — смело пишите в комментариях. Желаю всем поменьше ошибок в коде и побольше интересных проектов!
ссылка на оригинал статьи https://habr.com/ru/post/652035/
Добавить комментарий