autotelik/datashift

View on GitHub
lib/datashift/loaders/paperclip/attachment_loader.rb

Summary

Maintainability
B
4 hrs
Test Coverage
# Copyright:: (c) Autotelik Media Ltd 2012
# Author ::   Tom Statter
# Date ::     Sept 2012
# License::   MIT. Free, Open Source.
#
# => Provides facilities for bulk uploading/exporting attachments provided by PaperClip gem
#
require 'loader_base'
require 'datashift_paperclip'

module DataShift

  module Paperclip

    class AttachmentLoader < LoaderBase

      include DataShift::Paperclip

      attr_accessor :attach_to_klass, :attach_to_find_by_field, :attach_to_field

      # We try splitting up inbound file_names in various ways looking for the attachment Owner
      attr_accessor :split_file_name_on

      attr_reader :loading_files_cache, :missing_records

      def initialize
        super

        @attach_to_klass = nil
        @attach_to_find_by_field = nil
        @attach_to_field = nil

        @split_file_name_on = Regexp.new(/\s+/)

        @missing_records = []
      end

      # => :attach_to_klass
      #       A class that has a relationship with the attachment (has_many, has_one or belongs_to etc)
      #       The instance of :attach_to_klass can be searched for and the new attachment assigned.
      #
      #     Examples
      #       Owner has_many pdfs and mp3 files as Digitals .... :attach_to_klass = Owner
      #       User has a single image used as an avatar ... :attach_to_klass = User
      #
      # => :attach_to_find_by_field
      #       Field on the :attach_to_klass, this is the field used to search for the
      #       object (class == attach_to_klass) to assign the new attachment to.
      #
      #     Examples
      #       Owner has a unique 'name' field ... :attach_to_find_by_field = :name
      #       User has a unique  'login' field  ... :attach_to_klass = :login
      #
      # => :attach_to_field
      #       Attribute/association to assign attachment to on :attach_to_klass.
      #      Examples
      #
      #         :attach_to_field => digitals  : Owner.digitals = attachment
      #         :attach_to_field => avatar    : User.avatar = attachment
      #
      def init(attach_to_klass, attach_to_find_by_field, attach_to_field, options = {})

        @attach_to_klass = MapperUtils.ensure_class( attach_to_klass )

        ModelMethods::Manager.catalog_class(@attach_to_klass, reload: options[:reload], instance_methods: true)

        @attach_to_find_by_field = attach_to_find_by_field
        @attach_to_field = attach_to_field
      end

      def init_from_options(options)
        init(options[:attach_to_klass], options[:attach_to_find_by_field], options[:attach_to_field])

        @split_file_name_on = options[:split_file_name_on] if(options[:split_file_name_on])
      end

      def find_bindings
        # Map field to a suitable call on the Active Record Owner class e.g Owner.digitals
        bindings = begin
          logger.info("Finding matching field/association [#{attach_to_field}] on class [#{attach_to_klass}]")

          binder.map_inbound_fields(attach_to_klass, attach_to_field)
        rescue StandardError => e
          logger.error("Failed to map #{attach_to_field} to database operator : #{e.inspect}")
          logger.error( e.backtrace )
          raise MappingDefinitionError, 'Failed to map #{attach_to_field} to database operator'
        end

        bindings
      end

      def find_owner(in_file_name, options = {} )

        search_term = File.basename(in_file_name, '.*')
        search_term.strip!

        logger.info("Attempting to find attachment owner (#{attach_to_klass} for [#{search_term}]")

        owner_record = get_record_by(attach_to_klass, attach_to_find_by_field, search_term, split_file_name_on, options)

        if owner_record
          logger.info("#{owner_record.class} (id : #{owner_record.id}) found with matching :#{attach_to_find_by_field} ")
        else

          message = "No matching owner found for file name : #{search_term}"

          failed = FailureData.new(nil, doc_context.node_context, message)

          doc_context.progress_monitor.failure(failed)

          missing_records << in_file_name
        end

        owner_record
      end

      # This version creates attachments and also attaches them to instances of :attach_to_klazz
      #
      # Each file found in PATH will be processed - it's file_name being used to scan for
      # a matching record to attach the file to.
      #
      # Options
      #   :add_prefix
      #
      def perform_load(options = {} )

        raise 'The class that attachments belongs to has not been set (:attach_to_klass)' unless @attach_to_klass

        raise "The field to search for attachment's owner has not been set (:attach_to_find_by_field)" unless @attach_to_find_by_field

        is_dummy_run = DataShift::Configuration.call.dummy_run

        @missing_records = []

        @load_object = options[:attachment] if options[:attachment]

        # Support both directory and file
        @loading_files_cache = DataShift::Paperclip.get_files(file_name, options)

        logger.info("Found #{loading_files_cache.size} files - splitting names on delimiter [#{split_file_name_on}]")

        # Map field to a suitable call on the Active Record Owner class e.g Owner.digitals
        bindings = find_bindings

        attach_to_method_binding = if bindings.size != 1
                                     logger.warn("Failed to map #{attach_to_field} to database operator")
                                     nil
                                   else
                                     bindings[0]
                                   end

        # Iterate through all the files creating an attachment per file

        loading_files_cache.each do |in_file_name|
          attachment_name = File.basename(in_file_name)

          logger.info "Processing attachment file #{attachment_name} "

          owner_record = find_owner(in_file_name, options)

          # Don't actually create/upload to DB if we are doing dummy run
          # Don't create the Attachment if the Owner was not found

          next if(is_dummy_run || owner_record.nil?)

          attachment = create_paperclip_attachment(load_object_class, in_file_name, options)

          # Check if attachment must have an associated owner_record
          next unless attachment && owner_record && attach_to_method_binding
          reset

          # TOFIX - what about has_one etc ? - indicates that Context etc are still too complex and Excel/CSV focused
          owner_record.send(attach_to_method_binding.operator) << attachment

          logger.info "Added Attachment to #{owner_record.class} (id : #{owner_record.id})"
        end

        process_missing_records( missing_records )

        created = loading_files_cache.size - missing_records.size

        puts "Created #{created} / #{loading_files_cache.size} attachments of type #{load_object_class} attached to #{@attach_to_klass}"

        puts 'Dummy Run Complete - if happy run with full commit' if(is_dummy_run)

      end

    end

    def process_missing_records( missing_records )
      unless missing_records.empty?
        FileUtils.mkdir_p('MissingAttachmentRecords') unless File.directory?('MissingAttachmentRecords')

        puts "WARNING : #{missing_records.size} of #{loading_files_cache.size} files could not be attached to a #{load_object_class}"
        puts "For your convenience copying files with MISSING #{attach_to_klass} to : MissingAttachmentRecords"
        missing_records.each do |i|
          logger.info("Copying #{i} to MissingAttachmentRecords folder")
          FileUtils.cp( i, 'MissingAttachmentRecords') unless(DataShift::Configuration.call.dummy_run)
        end
      end
    end

  end
end