Управління складністю в проектах на ruby on rails. Частина 3

попередній частині я розповів про контролери та роутинг. Тепер поговоримо про форми. Досить часто потрібно реалізувати форми, яким не відповідає жодна модель. Або додати валідацію, яка має зміст лише в конкретному бізнес-процесі.
Я розповім про 2 типи форм: form-objects і types.
Объкты-форми використовуються для обработи та валідації користувальницького введення, коли дані потрібні для будь-якої операції. Наприклад, вхід користувача в систему або фільтрація даних.
Types використовуються, якщо потрібно розширити поведінку моделі. Наприклад, у вашому проекті користувачі можуть реєструватися як через vkontakte, так і через звичайну форму. Заповнення email обов'язково для звичайних користувачів, а для vk користувачів — ні. Така поведінка легко вирішується за допомогою types.
Form-objects
У RoR проектах форми жорстко зав'язані на моделі. Отрендерить складну форму без об'єкта-моделі практично неможливо та й не зручно. З цього об'єкти-форми розширюються з допомогою ActiveModel::Model. Таким чином форми — це моделі без підтримки persistence (не зберігаються в БД). Відповідно ми отримаємо безшовну інтеграцію з билдером форм, валідації, локалізацію.
Для зручності роботи об'єкти-форми так само використовують gem virtus. Він бере на себе приведення типів, виставляє значення за замовчуванням. Наприклад, якщо з форми приходить дата у строковому поданні, то virtus автоматично перетворює її в дату.
# Базовий клас для всіх форм
# app/forms/base_form.rb
class BaseForm
include Virtus.model(strict: true)
include ActiveModel::Model
end

# app/forms/user/statistics_filter_form.rb
class User::StatisticsFilterForm < BaseForm
attribute :start_date, ActiveSupport::TimeWithZone, default: ->(*) { DateTime.current.beginning_of_month }
attribute :end_date, ActiveSupport::TimeWithZone, default: ->(model, _) { model.start_date.next_month }
end

# app/controllers/web/users/statistics_controller.rb
class Web::Users::StatisticsController < Web::Users::ApplicationController
def show
# тут не обов'язково використовувати permits, т. к. це актуально тільки для моделей active_record
@filter_form = User::StatisticsFilterForm.new params[:user_statistics_filter_form]
@statistics = UserStatisticsQuery.perform resource_user, @filter_form.start_date, @filter_form.end_date
end
end

= simple_form_for @filter_form, method: :get url: {} do |f|
= f.input :start_date, as: :datetime_picker
= f.input :end_date, as: :datetime_picker
= f.button :submit

Розглянемо ситуацію складніше. У нас є форма входу в систему з двома полями: email і пароль. Поля обов'язкові для заповнення. Так само якщо користувач не знайдений або пароль не підійшов, повинна виводитися відповідна помилка.
# app/forms/session_form.rb
class SessionForm < BaseForm
attribute :email
attribute :password

validates :email, email: true
validates :password, presence: true

# додаємо валідацію для випадку, якщо користувач не знайдений або пароль не підійшов
validate do
errors.add(:base :wrong_email_or_password) unless user.try(:authenticate, password)
end

def user
@user ||= User.find_by email: email
end
end

# app/controllers/web/sessions_controller.rb
class Web::SessionsController < Web::ApplicationController
def new
@session_form = SessionForm.new
end

def create
@session_form = SessionForm.new session_form_params

# форма бере на себе всю валідацію
if @session_form.valid?
sign_in @session_form.user
redirect_to root_path
else
render :new
end
end

private

def session_form_params
params.require(:session_form).permit(:email :password)
end
end

У цьому прикладі форма бере на себе всі турботи про валідації вхідних даних, контролер не містить зайвої логіки, а модель тільки перевіряє пароль.
З таким підходом дуже просто реалиовать додатковий функціонал: вивести чекбокс "запам'ятати мене", блокувати користувачів з чорного списку.
Types
Якщо я не помиляюся, Types прийшли з symfony. Type — це спадкоємець моделі, який видає себе за батьків і додає новий функціонал.
Розглянемо таку задачу: користувачі програми можуть запрошувати користувачів тільки рангом нижче себе. Так само прглашающий не повинен знати пароль запрошуваного. Список ролей користувачів, які можна призначити новому користувачеві визначаються політикою. У частині про ACL я докладніше про це розповім.
module BaseType
extend ActiveSupport::Concern

class_methods do
def model_name
superclass.model_name
end
end
end

class InviteType < User
include BaseType

after_initialize :generate_password, if: :new_record?

validates :role, inclusion: { in: :available_roles }
validates :inviter, presence: true # запрошуючий

def policy
InvitePolicy.new(inviter, self)
end

def available_roles
policy.available_roles
end

def available_role_options
User.role.options.select{ |option| option.last.in? available_roles }
end

private

def generate_password
self.password = SecureRandom.urlsafe_base64(6)
end
end

InviteType перевіряє наявність запрошує, геренирует пароль і обмежує список доступних ролей.
Докладніше зупинюся на BaseType. Він перевизначає метод model_name, що б type сприймався як батьківський об'єкт. Не варто ігнорувати метод name, т. к. ruby з-за цього зносить дах. Тут є тонкість при роботі з STI: потрібно додатково перевизначити метод sti_name.
Маючи об'єкти-форми і types зручно трансформувати дані, що надходять з форми. Наприклад, у формі є 2 поля: затраченно годин, витрачено хвилин, а модель зберігає затрачений час в секундах.
class CommentType < Comment
include BaseType

# some code

def elapsed_time_hours
TimeConverter.convert_to_time(elapsed_time.to_i)[:hours]
end

def elapsed_time_hours=(v)
update_elapsed_time v.to_i, elapsed_time_minutes
end

def elapsed_time_minutes
TimeConverter.convert_to_time(elapsed_time.to_i)[:minutes]
end

def elapsed_time_minutes=(v)
update_elapsed_time elapsed_time_hours, v.to_i
end

private

def update_elapsed_time(годин, хвилин)
self.elapsed_time = TimeConverter.convert_to_seconds(hours: hours, minutes: minutes)
end
end

ПС. Стаття була написана більше року тому і просто пролежала в архіві. За цей час я виклав в загальний доступ проект, на основі якого писався цей цикл статей. Деякі речі я б зробив інакше, проте в ньому є цікаві й актуальні рішення.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.