rossta/lionel_richie

View on GitHub
lib/lionel/export.rb

Summary

Maintainability
A
3 hrs
Test Coverage
module Lionel
  class Export
    include Configurable

    attr_reader :options

    config_accessor :google_doc_id, :trello_board_id

    def self.builder=(builder)
      @builder = builder
    end

    def self.builder
      @builder || ExportBuilder.default
    end

    def initialize(options = {})
      @options = options
    end

    def data
      {
        trello_board_id: trello_board_id,
        google_doc_id: google_doc_id
      }
    end

    def board
      @board ||= Trello::Board.find(trello_board_id)
    end

    def cards
      # iterate over active lists rather
      # than retrieving all historical cards;
      # trello api returns association proxy
      # that does not respond to "flatten"
      @cards ||= begin
        case options.fetch('filter', 'open-lists')
        when 'open-cards'
          retrieve_open_cards
        when 'open-lists'
          retrieve_open_cards_in_open_lists
        end.map { |c| Lionel::ProxyCard.new(c) }
      end
    end

    def spreadsheet
      @spreadsheet ||= google_session.spreadsheet_by_key(google_doc_id)
    end

    def worksheet
      @worksheet ||= get_worksheet
    end

    def process
      download

      if options['print']
        Lionel.logger.info "DRY RUN..."
        Lionel.logger.info "Results were not uploaded to Google Drive"
      else
        Lionel.logger.info "Uploading..."
        upload
        Lionel.logger.info "Done!"
      end
    end

    def download
      raise_missing_builder_error unless builder

      Lionel.logger.info "Exporting trello board '#{board.name}' (#{trello_board_id}) to " + "google doc '#{spreadsheet.title}' (#{google_doc_id})"

      card_map.each do |row, card|
        begin
          Timeout.timeout(5) do
            row_data = card_columns(card).map do |col, value|
              worksheet[col,row] = value
            end.join(" | ")
            Lionel.logger.info "row[#{row}]: " + row_data
          end
        rescue Timeout::Error, Trello::Error => e
          Lionel.logger.warn e.inspect
          Lionel.logger.warn card.inspect
        end
      end
    end

    def card_map
      @card_map ||= CardMap.new(cards, worksheet)
    end

    def upload
      worksheet.save
    end

    def rows
      worksheet.rows
    end

    def builder
      self.class.builder
    end

    def card_columns(card)
      card_column_rows[card.id] ||= {}.tap do |columns|
        builder.columns.each do |col_name, block|
          columns[col_name] = card.instance_exec(self, &block)
        end
      end
    end

    def card_column_rows
      @card_column_rows ||= {}
    end

    def authenticate
      return if @authenticated
      authenticate_trello
      authenticate_google
      @authenticated
    end

    def authenticate_trello
      trello_session.configure
      @board = Trello::Board.find(trello_board_id)
    end

    def authenticate_google
      google_session
      @worksheet = Lionel::ProxyWorksheet.new(spreadsheet.worksheets[0])
    end

    def google_session
      @google_session ||= GoogleDrive.login_with_oauth(configuration.google_token)
    end

    def trello_session
      @trello_session ||= TrelloAuthentication.new
    end

    def retrieve_open_cards
      board.cards(filter: :open)
    end

    def retrieve_open_cards_in_open_lists
      [].tap do |c|
        board.lists(filter: :open).each do |list|
          list.cards(filter: :open).each do |card|
            c << card
          end
        end
      end
    end

    def raise_missing_builder_error
      message = <<-ERROR.gsub(/^ {6}/, '')
        The export is not configured. Example:

        Lionel.export do
          A { id }
          B { name }
          C { url }
        end
      ERROR
      raise MissingBuilderError.new(message)
    end

    class CardMap
      include Enumerable

      attr_reader :cards, :worksheet

      def initialize(cards, worksheet)
        @cards, @worksheet = cards, worksheet
      end

      def each(&block)
        rows.each(&block)
      end

      def rows
        @rows ||= populate_rows
      end

      private

      def populate_rows
        {}.tap do |card_rows|

          start_row = 2 # Currently assumes a header column
          rows = worksheet.size

          # Find existing rows for current cards
          (start_row..rows).each do |row|
            cell_id = worksheet["B",row]
            next unless cell_id.present?
            card = cards.find { |c| c.id == cell_id }
            next unless card.present?
            card_rows[row] = card
          end

          # Set available rows for new cards
          new_cards = cards - card_rows.values
          new_cards.each_with_index do |card, i|
            row = rows + i + 1
            card_rows[row] = card
          end
        end
      end
    end

  end
end