
Зачем и для кого эта статья?
Всем привет! Я 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.3Rails 6.1.4.6MySQL 8.0Node 10.19Yarn 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. Надеюсь вам всем было интересно читать эту статью. Если есть какие-то замечания либо же предложения - смело пишите в комментариях. Желаю всем поменьше ошибок в коде и побольше интересных проектов!
Sin2x
https://en.wikipedia.org/wiki/Diaspora_(social_network)