четверг, 25 мая 2017 г.

Couldn't find file 'jquery' with type 'application/javascript'

Данная ошибка возникла в Rails 5 проекте. Связана она с тем, что не удается найти файл jquery. Устранить эту ошибку можно по-разному, все зависит от того, куда именно вы хотите поместить зависимость от jquery.

Добавить зависимость от jquery в проект

1) В этом случае для устранения нужно добавить в Gemfile проекта:
gem 'jquery-rails'

2) Согласно инструкции по подключению гема jquery-rails, нужно еще добавить следующее в /app/assets/javascripts/application.js
//= require jquery
//= require jquery_ujs

3) В корневой папке проекта сделать bundle install.

Добавить зависимость от jquery в engine или гем

Если к проекту подключается ваш собственный engine или гем, то зависимость от jquery можно определить в нем.
1) Укажите зависимость в корневом файле с расширением .gemspec:
Gem::Specification.new do |s|
  # ...
  s.add_dependency 'jquery-rails'
end

2) Важно! Теперь подключите 'jquery-rails' в файле YOUR-GEM/lib/YOUR-GEM/engine.rb:
# Здесь вручную подключаем гем, указанный в зависимостях
require 'jquery-rails'

module YourGem # <-- тут название вашего гема
  class Engine < ::Rails::Engine
    # ...
  end
end

3) В корневой папке проекта сделать bundle install.

среда, 3 июня 2015 г.

Пример рефакторинга ruby-метода

Однажды в нашем проекте на Ruby on Rails в одном классе мне попался метод:
def self_payer
  if @paid_type == PaymentContact::TYPE_PERSONAL
    @self_payer ||= Payer.self_payer.is_developer.first || Payer.find_by_title!(SELF_PAYER)
  else
    if @business_type.title == Payer::BUSINESS_TYPE_ENTREPRENEUR
      @self_payer ||= Payer.self_payer.is_developer.first
    else
      @self_payer ||= Payer.find_by_title!(SELF_PAYER)
    end
  end
end

Здесь видно повторение Payer.self_payer.is_developer.first, которое мы вынесем в приватный метод first_developer.

Выражение @paid_type == PaymentContact::TYPE_PERSONAL использует переменную экземпляра класса @paid_type, а также выглядит громоздким, поэтому вынесем его в метод personal_payment? .

Выражение @business_type.title == Payer::BUSINESS_TYPE_ENTREPRENEUR тоже использует переменную экземпляра класса и также выглядит громоздко, выносим его в метод entrepreneur?.

Подумаем, пообщаемся с коллегами и выясним, что Payer.self_payer.is_developer.first всегда будет возвращать какой-то результат, поэтому уберем в первом if выражение: || Payer.find_by_title!(SELF_PAYER) .

Вот, что у нас получится в результате рефакторинга ruby метода:
def self_payer
  if personal_payment?
    @self_payer ||= first_developer
  else
    if entrepreneur?
      @self_payer ||= first_developer
    else
      @self_payer ||= Payer.find_by_title!(SELF_PAYER)
    end
  end
end

Поговорим с коллегами, чтобы понять суть этого метода. Оказывается, что он должен возвращать первого разработчика (first_developer), если платеж делает физ. лицо ИЛИ индивидуальный предприниматель. Исходя из этого, оставляем один if:
def self_payer
  if personal_payment? || entrepreneur?
    @self_payer ||= first_developer
  else
    @self_payer ||= Payer.find_by_title!(SELF_PAYER)
  end
end

В этом получившемся ruby методе переменная @self_payer повторяется. Поискав по классу, где этот метод расположен, мы выясняем, что эта переменная больше нигде не используется. Поэтому метод упрощается до такого вида:
def self_payer
  (personal_payment? || entrepreneur?) ? first_developer : Payer.find_by_title!(SELF_PAYER)
end

Таким образом, путем рефакторинга мы 11 строк исходного метода сократили до 3 путем пересмотра логики работы и вынесения излишне подробных действий в отдельные методы. Теперь метод self_payer читается легко: "Если платеж осуществляет ФЛ или ИП, то в качестве собственного юр. лица нужно вернуть первую попавшуюся компанию-разработчик (first_developer). В противном случае нужно найти плательщика по названию, сохраненному в константе класса (SELF_PAYER)".

среда, 15 апреля 2015 г.

Гем Haml-rails для работы с шаблонизатором haml

По умолчанию в веб-фреймворке Rails используются erb-шаблоны. Однако гораздо удобнее в использовании haml-шаблоны, синтаксис которых лаконичен.
Чтобы в Ruby on Rails включить поддержку haml-шаблонов, нужно в Gemfile добавить гем haml.
После этого ваши шаблоны можно называть так: app/views/products/index.html.haml.

Но что делать, если в нашем проекте уже используются erb-шаблоны? Вручную переписывать их на haml? Конечно, нет! Лучше воспользоваться гемом haml-rails.
Этот гем:
  • имеет команду для конвертации всех erb-шаблонов в haml-шаблоны:
    $ rake haml:erb2haml
    
  • позволяет делать scaffold генерацию с использованием haml-шаблонов.
    Команда
    $ rails g scaffold Order name address:text email pay_type 
    

    сгенерирует шаблоны в формате haml, а не erb, как это происходит по умолчанию.

пятница, 3 апреля 2015 г.

Пример оптимизации миграции в Ruby on Rails

Очень часто при разработке веб-приложения возникает необходимость внести изменения в структуру базы данных: добавить колонку, изменить ее тип, перенести значения из одной колонки в другую и т.п.
Для осуществления этих задач в веб-фреймворке Ruby on Rails существуют миграции.
$ rails g migration add_invoice_number_to_statements invoice_number:string{20}

Эта команда, выполненная в консоли, создаст файл миграции:
db/migrate/20150403113019_add_invoice_number_to_statements.rb
class AddInvoiceNumberToStatements < ActiveRecord::Migration
  def change
    add_column :statements, :invoice_number, :string, :limit => 20
  end
end

Предположим, что нам нужно, в колонку invoice_number еще поместить данные из другой колонки - invoice_id, имеющей тип int. Миграция может выглядеть так:
class AddInvoiceNumberToStatements < ActiveRecord::Migration
  def up
    add_column :statements, :invoice_number, :string, :limit => 20
    
    Statement.find_each do |statement|
      statement.update_column(:invoice_number, statement.invoice_id) if statement.invoice_id
    end
  end

  def down
    remove_column :statements, :invoice_number
  end
end

Вроде мы молодцы, в методе up написали условие if, благодаря которому не будет запроса на обновление поля invoice_number, если в поле invoice_id ничего нет, уменьшает общее число запросов к БД. Кроме того, мы использовали метод find_each, который загружает Statement пачками по 1000 штук, материализует их в объекты и обрабатывает. Это позволит нашей миграции не затратить много оперативной памяти. Какие мы молодцы!
Однако, это не так. Если в таблице statements 50 000 записей, то данная миграция приведет к 50 000 запросам UPDATE.

Иногда полезно миграцию писать на чистом добром SQL:
class AddInvoiceNumberToStatementsAndMigrateData < ActiveRecord::Migration
  def up
    add_column :statements, :invoice_number, :string, :limit => 20

    execute <<-SQL
      COMMENT ON COLUMN statements.invoice_number IS 'Номер счет-фактуры';
      UPDATE statements SET invoice_number = invoice_id::text;
    SQL
  end

  def down
    remove_column :statements, :invoice_number
  end
end

А теперь мы молодцы - миграция отработает молниеносно. А создание комментария к полю является актом творения добра и еще одним поводом для гордости.

четверг, 2 апреля 2015 г.

undefined method `to_datetime' for nil:NilClass

Ошибка undefined method `to_datetime' for nil:NilClass может возникнуть в Ruby on Rails при попытке сравнить две даты, одна из которых nil.
Приведем пример такого кода:
cannot(:revert, Statement) do |statement|
  statement.date <= StatementDeadline.instance.date
end

Чтобы устранить эту ошибку, нужно перед сравнением убедиться в существовании дат:
cannot(:revert, Statement) do |statement|
  statement.date && StatementDeadline.instance.date && (statement.date <= StatementDeadline.instance.date)
end

Безусловно, это увеличивает объем кода, однако позволяет побороть ошибку undefined method `to_datetime' for nil:NilClass, которая может сломать шаблон. Теперь пользователь вместо того, чтобы созерцать ошибку, сможет увидеть часть нужной ему информации.

воскресенье, 29 марта 2015 г.

Примеры упрощения кода

Упрощать код очень важно, т.к. это увеличивает его читаемость. Одну и ту же задачу можно сделать одной, так и десятью строчками кода. Чем код компактнее, тем его легче поддерживать. Разумеется, не нужно доходить до фанатизма, пытаясь уместить все в одну строчку, т.к. это может привести к ухудшению его читаемости.

  1. lots.map { |lot| lot.start_date }.compact.min
    
    # Упростили
    lots.map(&:start_date).compact.min
    
  2. if age.present?
      I18n.t('firm.regular')
    else
      I18n.t('firm.new')
    end
    
    # Упростили:
    I18n.t("firm.#{age.present? ? :regular : :new}")
    
  3. if company.type_id == nil || company.type_id == CompanyType.online.id
      return false
    end
    
    # Упростили
    return false if [nil, CompanyType.online.id].include? company.type_id
    
  4. if companies.select { |company| company.store_type == company.online_type }.empty?
      return false
    end
    
    # Упростили
    return false if companies.none? { |company| company.store_type == company.online_type }
    

пятница, 27 марта 2015 г.

Rails: undefined method `to_datetime' for nil:NilClass

В Rails может возникнуть ошибка undefined method `to_datetime' for nil:NilClass при выполнении следующего кода:
filtered_products = products.select do |product|
  product.start_date && product.end_date &&
  (product.start_date <= start_date_ago && start_date_ago <= product.end_date) ||
  (start_date_ago <= product.start_date && product.start_date < first_start_date)
end

Здесь берутся товары и отбираются те из них, что попадают в нужный диапазон дат.
product.start_date - дата начала периода публикации товара
product.end_date - дата конца периода публикации товара
В начале мы проверяем, что у товара есть эти параметры, а уже после этого сравниваем их с датами first_start_date и start_date_ago, которые определены где-то выше по коду. Это позволяет нам избежать ошибки:
NoMethodError: undefined method `<' for nil:NilClass.

Почему же в приведенном выше коде возникает ошибка undefined method `to_datetime' for nil:NilClass ?

Это связано с тем, что мы по невнимательности нарушили расстановку скобок. Сначала нам нужно убедиться в наличии параметров product.start_date и product.end_date, а уже потом выполнять остальные сравнения:
filtered_products = products.select do |product|
  product.start_date && product.end_date &&
  ((product.start_date <= start_date_ago && start_date_ago <= product.end_date) ||
  (start_date_ago <= product.start_date && product.start_date < first_start_date))
end