abak-press / resque-reports

Make your custom reports to CSV in background using Resque with simple DSL
MIT License
0 stars 1 forks source link

Разработка DSL для построения фоновых отчетов #1

Open sclinede opened 11 years ago

sclinede commented 11 years ago

Вот пример существующего отчета "Лог заказов" в csv:

Дата КД;Город;Название компании;Ссылка на место КД;ID товара;Цена товара;Кол-во;Сумма заказа;Дата создания товара;Дата актуализации товара;Градусник товара;Рубрика;ID компании;Текст письма;Пакет;Сегмент;Тип конверсионного действия (КД);Ссылка
02.09.2013 05:47;Якутск;Тойота (Toyota) Центр Екатеринбург Запад;http://yakutsk.blizko.dev:8080/products/avtomobil_toyota_alphard_prestizh_akpp_3013156;3013156;2222000,0;1;2222000.0;13.08.2012 16:51;10.01.2013 04:33;90;Toyota Alphard;8630442;"Здравствуйте, mosova! \n\n Мы хотим заказать ""Автомобиль Toyota Alphard Престиж АКПП"" в количестве \n\n С уважением, Федор";1;Легковые автомобили;"";""
03.09.2013 11:23;Якутск;Тойота (Toyota) Центр Екатеринбург Запад;http://yakutsk.blizko.dev:8080/products/avtomobil_toyota_alphard_prestizh_akpp_3031877;3031877;2485000,0;1;2485000.0;13.08.2012 16:36;10.01.2013 04:33;90;Toyota Alphard;8630442;"Здравствуйте, mosova! \n\n Мы хотим заказать ""Автомобиль Toyota Alphard Престиж АКПП"" в количестве \n\n С уважением, Вася";1;Легковые автомобили;"";""

Хотелось бы достаточно просто формировать подобные отчеты, причем с возможностью выполнять формирование в фоне. На первый взгляд DSL для такой задачи видится следующим:

class OrderListsCSVReport < CSVReport
  source :orders

  directory File.join(Rails.root, 'public/files/shared/tmp/order_lists_report')  

  csv_options :col_sep => ','
  encoding CP1251

  table do |order|
    column 'Дата КД', (DateTime.parse(order['created_at']).in_time_zone.strftime('%d.%m.%Y %H:%M') if order['created_at'])
    column 'Город', :city
    column 'Название компании', order['company_name']
    column 'Ссылка на место КД', order['referer']
    column 'ID товара', order['product_id']
    column 'Цена товара', number_with_delimiter(order['product_price'], :separator => ',', :delimiter => '')
    column 'Кол-во', order['amount']
    column 'Сумма заказа', number_with_delimiter(order['total'], :separator => ',', :delimiter => '')
    column 'Дата создания товара', (DateTime.parse(order['product_created_at']).in_time_zone.strftime('%d.%m.%Y %H:%M') if order['product_created_at'])
    column 'Дата актуализации товара', (DateTime.parse(order['product_actualized_at']).in_time_zone.strftime('%d.%m.%Y %H:%M') if order['product_actualized_at'])
    column 'Градусник товара', order['product_rate']
    column 'Рубрика', order['rubric_title']
    column 'ID компании', order['company_id']
    column 'Текст письма', order['content']
    column 'Пакет', order['packet']
    column 'Сегмент', order['segment']
    column 'Тип КД', order['conversion_type']
    column 'Ссылка', order['lead_url']
  end

  create do |date_from, date_to|
    @date_from = date_from
    @date_to = date_to
  end

  def city(order)
    order['company_main_region_name']
  end

  def orders
    ...# запрос данных
  end
end

# работа с отчетом:
report = OrderListsCSVReport.new(date_from, date_to)
if report.exists? # считаю алиас ready? надо оставить, хорошо читается
  send_data(File.read(report.file_name),...)
else
  @job_id = report.bg_build
end
sclinede commented 11 years ago

@dkron, @vkuznetsov, @Strech жду советов и предложений

sclinede commented 11 years ago

Есть мысль использовать instance_eval, report.column -> column, будет медленнее, конечно, но проще с т.з. DSL

sclinede commented 11 years ago

Еще можно сделать lazy_load, для data_source. т.е. принимать не результат метода, а некий объект с известным интерфейсом.. Позволит еще кстати для некоторых отчетов не выгружать все данные за раз, а делать это итеративно => прогресс будет нагляднее.

Например так:

      @order_lists_report = Resque::Reports::Report.new(
        directory: File.join(Rails.root, 'public/files/shared/tmp/order_list'),
        file_name: "order-list-#{date_from}-#{date_to}.csv",
        data: { 
          source: OrderListReport.new, 
          lazy_load: :find_all, date_from, date_to
        }
        format: {
          coding: 'windows-1251'
          type: 'csv',
          options: {
            col_sep: ';',
            row_sep: "\r\n"
          }
        }
      )
Strech commented 11 years ago

Мне не нравится вот эта часть

@order_lists_report = Resque::Reports::Report.new(
        directory: File.join(Rails.root, 'public/files/shared/tmp/order_list'),
        file_name: "order-list-#{date_from}-#{date_to}.csv",
        data_source: find_all(date_from, date_to),
        format: {
          coding: 'windows-1251'
          type: 'csv',
          options: {
            col_sep: ';',
            row_sep: "\r\n"
          }
        }
      )
  1. Очень сложно
  2. Непонятно
  3. Очень много чего передавать и определять
vkuznetsov commented 11 years ago

Какие задачи будет решать твой DSL? DSL только для одного конкретного отчета? Если он универсальный покажи пример.

sclinede commented 11 years ago

Согласен.. может так:

  report_file = Resque::Reports::File.new(
     directory: File.join(Rails.root, 'public/files/shared/tmp/order_list'),
     file_name: "order-list-#{date_from}-#{date_to}.csv",
  )
  report_format = Resque::Reports::Format.new(
    coding: 'windows-1251'
    type: 'csv',
    options: {
      col_sep: ';',
      row_sep: "\r\n"
    }
  )
  @order_lists_report = Resque::Reports::Report.new(
        file: report_file,
        format: report_format,
        data_source: find_all(date_from, date_to)
      )

и если так, то их можно где-то в одном месте определять, потом повторно использовать. Например:

  CSV_1251_FORMAT = Resque::Reports::Format.new(
    coding: 'windows-1251'
    type: 'csv',
    options: {
      col_sep: ';',
      row_sep: "\r\n"
    }
  )

  XLS_UTF8_FORMAT = Resque::Reports::Format.new(coding: 'utf-8', type: 'xls')
sclinede commented 11 years ago

to @vkuznetsov: нет, конечно DSL - для формирования произвольного отчета Показать пример результата или кода для создания?

vkuznetsov commented 11 years ago

Пример использования. Я пользователь твоего DSL, хочу понять как им пользоваться и чем он может мне помочь.

Strech commented 11 years ago

@sclinede Покажи как сейчас и покажи как будет, например

dkron commented 11 years ago

с объявлением репорта намудрил. да. зачем передавать какие-то formats, в которых в свою очередь закладывается тип отчета. в контексте того, что все csv у нас выгружаются с одинаковой конфигурацией, мне больше нравился подход с CSVWriter, который в свою очередь уже можно конфигурировать при желании.

dkron commented 11 years ago

и опиши пожалуйста схематично структуру в целом, я видел первоначальный вариант и примеры отсюда уже с ним расходятся весьма ощутимо.

sclinede commented 11 years ago

to @vkuznetsov: вот как бы выглядела выгрузка товаров в csv из kirby-hoover

class ExportProductsCSVReport
  def initialize(company)
    products_scope = company.products.for_owners.includes(:product_properties, :rubric, :main_image)

    report_file = Resque::Reports::File.new(
       directory: File.join(Rails.root, 'public/files/shared/tmp/kirby_csv_export'),
       file_name: "#{@company.id}.csv",
    )

    @products_csv_report = Resque::Reports::Report.new(
      file: report_file,
      format: Resque::Reports::Format::CSV_1251,
      data_source: products_scope
    )
  end

  def build
    @products_csv_report.build do |product|
      column 'Наименование товара', product.name
      column 'Цена товара', product.product_properties.price
      column 'Краткое описание', product.announce
      column 'Валюта', Currency.title product.product_properties.currency
      column 'Полное описание', product.description
      column 'Наличие', (product.exists ? 'в наличии' : 'под заказ')
      column 'Ссылка на изображение товара/услуги' do
        path = product.main_image.try(:img).try(:url)

        "http://#{HOST}#{path}" if path
      end
      column 'Рубрика', (predl_path(:host => HOST, :path => product.rubric.path) if product.rubric)
      column 'Условия оплаты', product.paid_conditions
      column 'Сроки доставки', product.delivery_time    
      column 'ID товара' do
        # если встретился товар без yml_id, то нужно присвоить ему новый yml_id
        if product.yml_id.blank?
          product.update_attribute(:yml_id, product.id)
        end

        product.yml_id || Digest::SHA1.hexdigest(product.name)
      end
    end
  end # #build
end # class ExportProductsCSVReport

# Пример использования тот же, а именно:
  report = ExportProductsCSVReport.new(company).build
  if report.ready?
    send_data(File.read(report.file_name),...)
  else
    @job_id = report.progress_job_id
  end
sclinede commented 11 years ago

to @dkron: мы здесь только DSL обсуждаем, так что считаю обсуждение внутренностей здесь будет оффтопом

sclinede commented 11 years ago

уродливо выглядит "декорация" значений для столбцов.. можно их впринципе в методы выносить и вызывать метод

# было
column 'ID товара' do
   # если встретился товар без yml_id, то нужно присвоить ему новый yml_id
   if product.yml_id.blank?
     product.update_attribute(:yml_id, p.id)
   end

   product.yml_id || Digest::SHA1.hexdigest(product.name)
end

# стало
column 'ID товара', product_id(product)
...
def product_id(p)
   # если встретился товар без yml_id, то нужно присвоить ему новый yml_id
   if p.yml_id.blank?
     p.update_attribute(:yml_id, p.id)
   end

   p.yml_id || Digest::SHA1.hexdigest(p.name)
end
sclinede commented 11 years ago

to @Strech: можешь посмотреть ExportProductsCSV в blizko/vendor/plugins/kirby-hoover, сколько кода там написано и что нужно знать о resque для того чтобы реализовать ту же функцию. Просто много кода, думаю нет смысла его сюда приводить.

sclinede commented 11 years ago

в моей реализации хочется спрятать от пользователя знания о resque-integration, сообщать ему только job_id для наблюдения за прогрессом либо вообще сразу выдать уже ранее сформированный отчет.

vkuznetsov commented 11 years ago

А где здесь эта.. инкапсуляция? data_source - чем должен быть? Зачем в клиентском коде каждый раз писать Resque::Reports::File.new, Resque::Reports::Report.new ?

вот так примерно можно сделать?

class MyReport < CSVReport
  source :products
  directory: File.join(Rails.root, 'public/files/shared/tmp/kirby_csv_export')
  csv_options :delimiter => ',' ....
  encoding: CP1251

  table(ну или другое слово) do |product|
    column 'Наименование товара', product.name
  end

  def products
    @company.products
  end
end
sclinede commented 11 years ago

to @vkuznetsov: да была изначально именно такая идея, но после разговора с @Strech начал думать в сторону DSL и ушел слишком)

Твой вариант мне нравится, source здесь Enumerable, согласен с наследованием проще получается. table - мне нравится. Без опечаток - вот так?

class MyReport < CSVReport
  source :products
  directory File.join(Rails.root, 'public/files/shared/tmp/kirby_csv_export')
  file "#{@company.id}.csv"
  csv_options :delimiter => ',' ....
  encoding CP1251

  table do |product|
    column 'Наименование товара', product.name
  end

  def products
    @company.products
  end
end
sclinede commented 11 years ago

по file планирую делать уникальность задачи в очереди

vkuznetsov commented 11 years ago

откуда @company там возьмешь? уникальность, я считаю, надо делать по аргументам, передаваемым в конструктор

file сразу предлагаю заменить на filename и пусть эти методы могут принимать значение или символ. Если передан символ, то вызывается метод инстанса. Тогда можно будет написать:

filename :report_name

def report_name
  "#{@company.id}.csv"
end
sclinede commented 11 years ago

to @vkuznetsov про уникальность: ну а как же кеширование. если имя у нас не будет идентификатором отчета по заданным атрибутам, то мы всегда будем заново создавать отчет, даже если у нас только что был создан аналогичный. мне кажется идея с именем корректна, только вот использовать так

unique_filename :report_name

def report_name
  "#{@company.id}.csv"
end
sclinede commented 11 years ago

а про использование что думаешь? так и оставить?

  report = OrderListsCSVReport.new(date_from, date_to).build
  if report.ready?
    send_data(File.read(report.file_name),...)
  else
    @job_id = report.progress_job_id
  end
sclinede commented 11 years ago

@Strech @vkuznetsov - обновил topic

Strech commented 11 years ago

filename :report_name читается лучше чем unique_filename :report_name

vkuznetsov commented 11 years ago
report = OrderListsCSVReport.new(date_from, date_to)
if report.exists?
  ...
else
  @job_id = report.bg_build
end

почему нельзя кэшировать по аргументам?

sclinede commented 11 years ago

а как ты именно кэшировать предлагаешь ? я хотел по простому, проверяю наличие файла и дату создания = закешированный файл, а ты предлагаешь хранить инфу о параметрах где-то еще ?

sclinede commented 11 years ago

я согласен, что по аргументам - логичнее. и не надо на пользователя возлагать лишних умозаключений.. только вот теперь и случай с одинаковыми именами надо обработать. и доп. информацию где-то хранить.

sclinede commented 11 years ago

можно впринципе отказаться от filename, можно аргументы конструктора хешировать и все. 2х зайцев. прямые ссылки не используются.

sclinede commented 11 years ago

обновил topic @Strech @vkuznetsov

vkuznetsov commented 11 years ago

Можно еще учесть случай параллельных запросов. Когда уже файл есть, но еще не заполнен до конца.

sclinede commented 11 years ago

ну да. тогда тоже надо возвращать job_id, дабы за прогрессом наблюдать..

sclinede commented 11 years ago

достаточно ли будет проверки enqueued? в данном случае или придется где-то хранить ссылку на job_id для данного файла?

vkuznetsov commented 11 years ago

Ну это ты сам подумай как лучше сделать. Хорошо бы иметь увереноость в том что предыдущий писатель доделал свою работу до конца. Это не критично, но полезно. Надобность этого оцени по формуле полезность / сложность. Еще нужно подумать о способе удаления старых файлов.

sclinede commented 11 years ago

Да, это само собой. итак @dkron, @Strech, @vkuznetsov считаем DSL хорошим ? см. topic

Strech commented 11 years ago

Смущает вот это еще

order['rubric_title'].to_s.encode('cp1251', :invalid => :replace, :undef => :replace)
sclinede commented 11 years ago

можно тоже спрятать. согласен, если задали формат CP1251, все строки приведем к данной кодировке

sclinede commented 11 years ago

обновил @dkron, @Strech, @vkuznetsov

vkuznetsov commented 11 years ago

Считаю нужно чтобы column тоже мог принимать символ. Тогда запускается метод инстанса и ёму аргументом передаётся значение итератора.

sclinede commented 11 years ago

ну да, чтобы не писать order каждый раз, согласен

sclinede commented 11 years ago

на приведенном в топике примере не показательно получается

vkuznetsov commented 11 years ago

добавь column :company_name для примера

vkuznetsov commented 11 years ago

или счётчик - номер товара в выгрузке

sclinede commented 11 years ago

@Strech @vkuznetsov @dkron если есть возможность прошу активно покомментировать еще сегодня ибо я рано утром начинаю работать, вот, хотелось бы сегодня собрать инфы побольше чтобы утром уже сделать чего-нибудь)

vkuznetsov commented 11 years ago

пили уже )

Strech commented 11 years ago

@sclinede Да вроде уже более-менее хорошо, попробуй первую версию чтоли :1234:

sclinede commented 11 years ago

@Strech @vkuznetsov надо каким-то образом определять основные параметры отчета (например: для лога заказов это даты с какого по какое число), не хочется это делать через конструктор, предлагаю сделать это через аргументы для source метода, чтобы знать, какие переменные туда передать:

# типа вот так:
source :get_data, args: [:date_from, :date_to]
...
attr_reader :date_from, :date_to
Strech commented 11 years ago

@sclinede Пример использования приведи, не понятно

sclinede commented 11 years ago

Типа вот так:

class OrderListsCSVReport < CSVReport
  source :orders, args: [:date_from, :date_to]

  directory File.join(Rails.root, 'public/files/shared/tmp/order_lists_report')  

  csv_options :delimiter => ',' ....
  encoding CP1251

  table do |order|
    column 'Дата КД', (DateTime.parse(order['created_at']).in_time_zone.strftime('%d.%m.%Y %H:%M') if order['created_at'])
    column 'Город', :city
    column 'Название компании', order['company_name']
    column 'Ссылка на место КД', order['referer']
    column 'ID товара', order['product_id']
    column 'Тип КД', order['conversion_type']
    column 'Ссылка', order['lead_url']
  end

  def city(order)
    order['company_main_region_name']
  end

  def initialize(date_from, date_to)
    @date_from, @date_to, @another_var = date_from, date_to, "some usefull message with #{date_from} and #{date_to}"
  end

  def orders(date_from, date_to)
    ...# запрос данных
  end
end
sclinede commented 11 years ago

Второй вариант, мне нравится меньше:

class OrderListsCSVReport < CSVReport
  source :orders

  directory File.join(Rails.root, 'public/files/shared/tmp/order_lists_report')  

  csv_options :delimiter => ',' ....
  encoding CP1251

  table do |order|
    column 'Дата КД', (DateTime.parse(order['created_at']).in_time_zone.strftime('%d.%m.%Y %H:%M') if order['created_at'])
    column 'Город', :city
    column 'Название компании', order['company_name']
    column 'Ссылка на место КД', order['referer']
    column 'ID товара', order['product_id']
    column 'Тип КД', order['conversion_type']
    column 'Ссылка', order['lead_url']
  end

  def city(order)
    order['company_main_region_name']
  end

  def initialize(date_from, date_to)
    @date_from, @date_to, @another_var = date_from, date_to, "some usefull message with #{date_from} and #{date_to}"
    super(@date_from, @date_to)
  end

  def orders
    ...# запрос данных с использованием @date_from, @date_to
  end
end
sclinede commented 11 years ago

мне нужно знать, какой набор параметров уникально идентифицирует данный отчет, для кеширования.