bulk_ops 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/app/assets/images/bulk_ops/github_logo.png +0 -0
  3. data/app/assets/javascripts/bulk_ops.js +14 -0
  4. data/app/assets/javascripts/bulk_ops/selections.js +24 -0
  5. data/app/assets/javascripts/selections.js +38 -0
  6. data/app/assets/javascripts/work_search.js +64 -0
  7. data/app/assets/stylesheets/bulk_ops.scss +99 -0
  8. data/app/controllers/bulk_ops/application_controller.rb +13 -0
  9. data/app/controllers/bulk_ops/github_authorization_controller.rb +33 -0
  10. data/app/controllers/bulk_ops/operations_controller.rb +481 -0
  11. data/app/jobs/bulk_ops/application_job.rb +4 -0
  12. data/app/mailers/bulk_ops/application_mailer.rb +6 -0
  13. data/app/models/bulk_ops/application_record.rb +5 -0
  14. data/app/views/bulk_ops/_bulk_ops_sidebar_widget.html.erb +15 -0
  15. data/app/views/bulk_ops/_github_auth_widget.html.erb +13 -0
  16. data/app/views/bulk_ops/operations/_bulk_ops_header.html.erb +4 -0
  17. data/app/views/bulk_ops/operations/_choose_fields.html.erb +22 -0
  18. data/app/views/bulk_ops/operations/_choose_notifications.html.erb +22 -0
  19. data/app/views/bulk_ops/operations/_git_message.html.erb +7 -0
  20. data/app/views/bulk_ops/operations/_ingest_options.html.erb +42 -0
  21. data/app/views/bulk_ops/operations/_operation_options.html.erb +38 -0
  22. data/app/views/bulk_ops/operations/_show_authorize.html.erb +13 -0
  23. data/app/views/bulk_ops/operations/_show_complete.html.erb +31 -0
  24. data/app/views/bulk_ops/operations/_show_draft.html.erb +20 -0
  25. data/app/views/bulk_ops/operations/_show_new.html.erb +2 -0
  26. data/app/views/bulk_ops/operations/_show_pending.html.erb +58 -0
  27. data/app/views/bulk_ops/operations/_show_running.html.erb +56 -0
  28. data/app/views/bulk_ops/operations/_show_verifying.html.erb +8 -0
  29. data/app/views/bulk_ops/operations/_show_waiting.html.erb +9 -0
  30. data/app/views/bulk_ops/operations/_update_draft_work_list.html.erb +45 -0
  31. data/app/views/bulk_ops/operations/_update_draft_work_search.html.erb +59 -0
  32. data/app/views/bulk_ops/operations/_update_options.html.erb +9 -0
  33. data/app/views/bulk_ops/operations/index.html.erb +51 -0
  34. data/app/views/bulk_ops/operations/new.html.erb +36 -0
  35. data/app/views/bulk_ops/operations/show.html.erb +7 -0
  36. data/config/routes.rb +25 -0
  37. data/db/migrate/20180926190757_create_github_credentials.rb +13 -0
  38. data/db/migrate/20181017180436_create_bulk_ops_tables.rb +40 -0
  39. data/lib/bulk_ops.rb +15 -0
  40. data/lib/bulk_ops/create_spreadsheet_job.rb +43 -0
  41. data/lib/bulk_ops/create_work_job.rb +14 -0
  42. data/lib/bulk_ops/delete_file_set_job.rb +15 -0
  43. data/lib/bulk_ops/engine.rb +6 -0
  44. data/lib/bulk_ops/error.rb +141 -0
  45. data/lib/bulk_ops/github_access.rb +284 -0
  46. data/lib/bulk_ops/github_credential.rb +3 -0
  47. data/lib/bulk_ops/operation.rb +358 -0
  48. data/lib/bulk_ops/relationship.rb +79 -0
  49. data/lib/bulk_ops/search_builder_behavior.rb +80 -0
  50. data/lib/bulk_ops/templates/configuration.yml +5 -0
  51. data/lib/bulk_ops/templates/readme.md +1 -0
  52. data/lib/bulk_ops/update_work_job.rb +14 -0
  53. data/lib/bulk_ops/verification.rb +210 -0
  54. data/lib/bulk_ops/verification_job.rb +23 -0
  55. data/lib/bulk_ops/version.rb +3 -0
  56. data/lib/bulk_ops/work_job.rb +104 -0
  57. data/lib/bulk_ops/work_proxy.rb +466 -0
  58. data/lib/generators/bulk_ops/install/install_generator.rb +27 -0
  59. data/lib/generators/bulk_ops/install/templates/config/github.yml.example +28 -0
  60. metadata +145 -0
@@ -0,0 +1,59 @@
1
+ <div id="add-works" >
2
+ <h3>Find Works to add to update</h3>
3
+
4
+ <form id="ajax-work-search">
5
+ <div>
6
+ <label for="collection-id"> Collection </label>
7
+ <select name="collection_id" id="collection-id"><%= options_for_select @collections.insert(0,["Any",""]) %> </select>
8
+ </div>
9
+ <div>
10
+ <label for="admin-set-id"> Administrative Set </label>
11
+ <select name="admin_set_id" id="admin-set-id"> <%= options_for_select @admin_sets.insert(0,["Any",""]) %> </select>
12
+ </div>
13
+ <div>
14
+ <label for="workflow_state"> Workflow Step </label>
15
+ <select name="workflow_state" id="workflow-state"> <%= options_for_select @workflow_states.insert(0,["Any",""]) %></select>
16
+ </div>
17
+
18
+ <div>
19
+ <label for="keyword"> keyword search </label>
20
+ <input id="keyword" type="text" name="q" />
21
+ </div>
22
+ </form>
23
+ <button id="add-works-search" type="button"> Search </button>
24
+
25
+ <div id="search-summary">
26
+ <%= form_tag("#{@operation.id}/edit") do %>
27
+ <div id="draft-search-controls">
28
+ <div id="all-results" style="display:none">
29
+ <p id="count"></p>
30
+ <input type="checkbox" name="add_all_results" id="add-all-results" value="true"/>
31
+ <label for="add-all-results">Add all search results to update</label>
32
+ </div>
33
+ <input type="submit" id="draft-update-add-works" class="show-with-results" value="Add to Update" />
34
+ </div>
35
+ <div id="draft-search-display" class="show-with-results">
36
+ <h4>Sample Results (first 15 results)</h4>
37
+ <button type="button" class="selections select-all">Select All</button>
38
+ <button type="button" class="selections select-none">Select None</button>
39
+ <ul id="search-sample">
40
+ <li id="template" class="hidden">
41
+ <input type="checkbox" name="added_work_ids[]" value=""/>
42
+ <%= image_tag("", height: '200', class: 'work-thumb') %>
43
+ <div class="work-meta">
44
+ <div class="work-title"></div>
45
+ <div class="work-desription"></div>
46
+ </div>
47
+ </li>
48
+ </ul>
49
+ </div>
50
+ <input class="prev-search-field" id="prev-collection-id" type="hidden" name="collection_id" value=""/>
51
+ <input class="prev-search-field" id="prev-admin-set-id" type="hidden" name="admin_set_id" value=""/>
52
+ <input class="prev-search-field" id="prev-workflow-state" type="hidden" name="workflow_state" value=""/>
53
+ <input class="prev-search-field" id="prev-keyword" type="hidden" name="keyword" value=""/>
54
+ <% end %>
55
+ </div>
56
+
57
+
58
+ </div>
59
+
@@ -0,0 +1,9 @@
1
+ <div id="update-options" class="options-panel bulk-ops-options">
2
+ <div>
3
+ <label for="file-method" data-toggle="tooltip" title="These options determine how the update handles files attached to updated objects. The default option leaves all files alone unless you include a column in your spreadsheet to add or remove specific files.">File Update Method:</label>
4
+ <select name="file_method" id="file-method">
5
+ <%= options_for_select @file_update_options, @operation.options['file_method'] || "remove-and-add" %>
6
+ </select>
7
+ </div>
8
+
9
+ </div>
@@ -0,0 +1,51 @@
1
+ <% provide :page_title, "List Bulk Operations" %>
2
+ <%= javascript_include_tag "bulk_ops" %>
3
+ <%= stylesheet_link_tag "bulk_ops" %>
4
+ <%= render "bulk_ops_header", title: "Manage Bulk Ingests & Updates" %>
5
+ <%= render "bulk_ops/github_auth_widget" %>
6
+
7
+ <%= form_tag("destroy_multiple") do %>
8
+ <div id="bulk-ops-index-header">
9
+ <input type="checkbox" id="bulk-ops-select-all" />
10
+ <button id="bulk-ops-delete-group">Delete Selected</button>
11
+ <a href="new" >
12
+ <button type="button" id="bulk-ops-new"> New Bulk Operation</button>
13
+ </a>
14
+ </div>
15
+
16
+ <ul id="operations-index">
17
+ <% if @active_operations.empty? %>
18
+ No bulk operations have been created yet.
19
+ <% end %>
20
+ <% @active_operations.each do |operation| %>
21
+ <li>
22
+ <input type="checkbox" name="operation_ids[]" class="bulk-ops-index-checkbox" value="<%= operation.id %>" />
23
+ <div class="op-name">
24
+ <%= link_to(operation.name,operation)%>
25
+ </div>
26
+
27
+ <div class="op-summary">
28
+ <div class="op-type">
29
+ <label> Type: </label>
30
+ <%=operation.type%>
31
+ </div>
32
+ <div class="op-stage">
33
+ <label> Stage: </label>
34
+ <%=operation.stage%>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="op-status">
39
+ <label> Status: </label>
40
+ <%=operation.status%>
41
+ </label>
42
+ </div>
43
+ <div class="op-message">
44
+ <label> Message: </label>
45
+ <%=operation.message%>
46
+ </div>
47
+
48
+ </li>
49
+ <% end %>
50
+ </ul>
51
+ <% end %>
@@ -0,0 +1,36 @@
1
+ <%= javascript_include_tag "bulk_ops" %>
2
+ <%= stylesheet_link_tag "bulk_ops" %>
3
+ <% provide :page_title, "New Bulk Operations" %>
4
+ <%= render "bulk_ops_header", title: 'Create New Bulk Ingest or Update' %>
5
+ <%= render "bulk_ops/github_auth_widget" %>
6
+
7
+ <%= form_tag "/bulk_ops", class: "new-bulk-op", id: "new-bulk-op" do %>
8
+
9
+ <div id="operation-name" class="bulk-ops-options">
10
+ <label data-toggle="tooltip" title="Choose a short descriptive name for this bulk operation" for="bulk-ops-name">New Operation Name:</label>
11
+ <input type="text" name="name" id="bulk-ops-name" />
12
+ </div>
13
+
14
+ <div class="bulk-ops-options">
15
+ <label for="op-type" data-toggle="tooltip" title="Would you like to set up a bulk ingest of new works, or a bulk update of existing works?">Ingest or Update?</label>
16
+ <select name="type" id="op-type" onchange="$('.bulk-ops-options.options-panel').hide();$('#operation-options').show(); $('#'+this.value+'-options').show()" >
17
+ <option disabled selected value> -- Choose Operation Type -- </option>
18
+ <option value="ingest">Ingest </option>
19
+ <option value="update">Update </option>
20
+ </select>
21
+ </div>
22
+
23
+ <%= render "operation_options" %>
24
+ <%= render "ingest_options" %>
25
+ <%= render "update_options" %>
26
+
27
+ <%= render "choose_fields" %>
28
+
29
+ <%= render "choose_notifications" %>
30
+
31
+ <%= render "git_message" %>
32
+
33
+ <input type="submit" name="create_operation" id="create-operation" value="Create Bulk Operation" />
34
+
35
+ <% end %>
36
+
@@ -0,0 +1,7 @@
1
+ <% provide :page_title, @operation.name.titleize %>
2
+ <%= javascript_include_tag "embed" %>
3
+ <%= javascript_include_tag "bulk_ops" %>
4
+ <%= stylesheet_link_tag "bulk_ops" %>
5
+ <%= render "bulk_ops_header", title: "Showing Bulk #{@operation.type.capitalize}: #{@operation.name.titleize}" %>
6
+
7
+ <%= render "show_#{@operation.stage}"%>
@@ -0,0 +1,25 @@
1
+ BulkOps::Engine.routes.draw do
2
+
3
+ get 'bulk_ops/authorize/:user_id', to: 'github_authorization#authorize'
4
+ post 'bulk_ops/github_logout/:user_id', to: 'github_authorization#logout'
5
+ post 'bulk_ops/apply', to: 'operations#apply'
6
+
7
+ resources :operations, path: :bulk_ops do
8
+
9
+ collection do
10
+ get :search
11
+ post :apply
12
+ post :search
13
+ post :destroy_multiple
14
+ end
15
+
16
+ member do
17
+ get :csv
18
+ post :request_apply
19
+ post :approve
20
+ post :edit
21
+ post :duplicate
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ class CreateGithubCredentials < ActiveRecord::Migration[5.0]
2
+ def change
3
+ create_table :bulk_ops_github_credentials do |t|
4
+ t.integer :user_id
5
+ t.string :username
6
+ t.string :oauth_token
7
+ t.string :state
8
+
9
+ t.timestamps
10
+ end
11
+ add_index :bulk_ops_github_credentials, :user_id
12
+ end
13
+ end
@@ -0,0 +1,40 @@
1
+ class CreateBulkOpsTables < ActiveRecord::Migration[5.0]
2
+ def change
3
+
4
+ create_table :bulk_ops_operations do |t|
5
+ t.references :user, foreign_key: true
6
+ t.string :name, null: false, unique: true
7
+ t.string :stage, null: false
8
+ t.string :operation_type
9
+ t.string :commit_sha
10
+ t.integer :pull_id
11
+ t.string :status
12
+ t.text :message
13
+ t.timestamps
14
+ end
15
+
16
+ create_table :bulk_ops_work_proxies do |t|
17
+ t.integer :operation_id
18
+ t.string :work_id
19
+ t.integer :row_number
20
+ t.datetime :last_event
21
+ t.string :status
22
+ t.text :message
23
+ t.string :visibility
24
+ t.string :work_type
25
+ t.string :reference_identifier
26
+ t.string :order
27
+ t.timestamps
28
+ end
29
+
30
+ create_table :bulk_ops_relationships do |t|
31
+ t.integer :work_proxy_id
32
+ t.string :object_identifier
33
+ t.string :identifier_type
34
+ t.string :relationship_type
35
+ t.string :status
36
+ t.timestamps
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,15 @@
1
+ require "bulk_ops/version"
2
+
3
+ module BulkOps
4
+ dirstring = File.join( File.dirname(__FILE__), 'bulk_ops/**/*.rb')
5
+ Dir[dirstring].each do |file|
6
+ begin
7
+ require file
8
+ rescue Exception => e
9
+ puts "ERROR LOADING #{File.basename(file)}: #{e}"
10
+ end
11
+ end
12
+ # require 'bulk_ops/verification'
13
+ # require 'bulk_ops/verification'
14
+ # require 'bulk_ops/work_proxy'
15
+ end
@@ -0,0 +1,43 @@
1
+ class BulkOps::CreateSpreadsheetJob < ActiveJob::Base
2
+
3
+ queue_as :default
4
+
5
+ def perform(branch_name, work_ids, fields, user)
6
+ csv_file = Tempfile.new('bulk_ops_metadata')
7
+ csv_file.write(fields.join(','))
8
+ work_ids.each do |work_id|
9
+ if work_csv = work_to_csv(work_id,fields)
10
+ csv_file.write("\r\n" + work_csv)
11
+ end
12
+ end
13
+ csv_file.close
14
+ BulkOps::GithubAccess.new(branch_name, user).add_new_spreadsheet(csv_file.path)
15
+ csv_file.unlink
16
+ end
17
+
18
+ private
19
+
20
+ def work_to_csv work_id, fields
21
+ return false if work_id.empty?
22
+ begin
23
+ work = Work.find(work_id)
24
+ rescue ActiveFedora::ObjectNotFoundError
25
+ return false
26
+ end
27
+ line = ''
28
+ fields.map do |field_name|
29
+ label = false
30
+ if field_name.downcase.include? "label"
31
+ label = true
32
+ field_name = field_name[0..-7]
33
+ end
34
+ values = work.send(field_name)
35
+ values.map do |value|
36
+ next if value.is_a? DateTime
37
+ value = (label ? WorkIndexer.fetch_remote_label(value.id) : value.id) unless value.is_a? String
38
+ value.gsub("\"","\"\"")
39
+ end.join(BulkOps::WorkProxy::SEPARATOR).prepend('"').concat('"')
40
+ end.join(',')
41
+ end
42
+
43
+ end
@@ -0,0 +1,14 @@
1
+ #require 'hydra/access_controls'
2
+ #require 'hyrax/workflow/activate_object'
3
+
4
+ require 'bulk_ops/work_job'
5
+
6
+ class BulkOps::CreateWorkJob < BulkOps::WorkJob
7
+
8
+ private
9
+
10
+ def type
11
+ :create
12
+ end
13
+
14
+ end
@@ -0,0 +1,15 @@
1
+ #require 'hydra/access_controls'
2
+ #require 'hyrax/workflow/activate_object'
3
+
4
+ class BulkOps::DeleteFileSetJob < ActiveJob::Base
5
+
6
+ queue_as :ingest
7
+
8
+ def perform(file_set_id,user_email)
9
+ user = User.find_by_email(user_email)
10
+ Hyrax::Actors::FileSetActor.new(@file_set, user).destroy
11
+ end
12
+
13
+ private
14
+
15
+ end
@@ -0,0 +1,6 @@
1
+ module BulkOps
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace BulkOps
4
+ engine_name 'bulk_ops'
5
+ end
6
+ end
@@ -0,0 +1,141 @@
1
+ class BulkOps::Error
2
+ attr_accessor :type, :row_number, :object_id, :message, :option_name, :file, :option_values, :field, :url
3
+
4
+ MAX_ERROR = 50
5
+
6
+ def initialize type:, row_number: nil, object_id: nil, message: nil, options_name: nil, option_values: nil, field: nil, url: nil , file: nil
7
+ @type = type
8
+ @row_number = row_number
9
+ @object_id = object_id
10
+ @message = message
11
+ @option_name = option_name
12
+ @option_values = option_values
13
+ @field = field
14
+ @file = file
15
+ @url = url
16
+ end
17
+
18
+ def self.write_errors! errors, git
19
+ return false if errors.blank?
20
+ error_file_name = "error_log_#{DateTime.now.strftime("%F-%H%M%p")}.log"
21
+
22
+ error_hash = {}
23
+
24
+ errors.sort!{|x,y| x.type <=> y.type}
25
+ error_types = errors.map{|error| error.type}.uniq
26
+
27
+ #write errors to error file
28
+ error_file = Tempfile.new(error_file_name)
29
+ error_types.each do |error_type|
30
+ typed_errors = errors.select{|er| er.type == error_type}
31
+ next if typed_errors.blank?
32
+ message = self.error_message(error_type, typed_errors)
33
+ puts "Error message: #{message}"
34
+ error_file.write(message)
35
+ end
36
+ error_file.close
37
+ git.add_file error_file.path, File.join("errors", error_file_name)
38
+ error_file.unlink
39
+ return error_file_name
40
+ end
41
+
42
+ def self.error_message type, errors
43
+ case type
44
+ when :mismatched_auth_terms
45
+ message = "\n-- Controlled Authority IDs and Labels don't match -- \n"
46
+ message += "The operation is set to create an error if the provided URLs for controlled authority terms do not resolve to the provided labels.\n"
47
+ if errors.count < MAX_ERROR
48
+ message += "The following rows were affected:\n"
49
+ message += errors.map{|error| error.row_number}.join(",")+"\n"
50
+ else
51
+ message += "#{errors.count} rows were affected. An example is row # #{errors.first.row_number}.\n"
52
+ end
53
+ when :upload_error
54
+ message = "\n-- Errors uploading files -- \n"
55
+ message += "Your files looked ok when we checked earlier, but we couldn't access them when we were trying to actually start the operation.\n"
56
+ if errors.count < MAX_ERROR
57
+ message += "The following files were affected:\n"
58
+ message += errors.map{|error| "Row #{row_number}, Filename: #{error.file}"}.join("\n")+"\n"
59
+ else
60
+ message += "#{errors.count} rows were affected. An example is row # #{errors.first.row_number} with file #{errors.first.file}.\n"
61
+ end
62
+
63
+ when :no_work_id_field
64
+ message = "\n-- Cannot find work id field in spreadsheet -- \n"
65
+ message += "We were trying to start your operation, but could find find the work id for #{errors.count} different rows of the spreadsheet.\n"
66
+ message += "Check your spreadsheet and try again.\n"
67
+ message += errors.map{|arg| " #{error.object_id || 'new work'}: #{error.message}"}.join("\n") + "\n"
68
+ when :job_failure
69
+ message = "\n-- Jobs Failed -- \n:"
70
+ message += errors.map{|arg| "Error message operating on #{error.object_id || 'new work'}: #{error.message}"}.join("\n") + "\n"
71
+ when :missing_required_option
72
+ message = "\n-- Errors in configuration file -- \nMissing required option(s):"
73
+ message += errors.map{|arg| error.option_name}.join(", ") + "\n"
74
+
75
+ when :invalid_config_value
76
+ message = "\n-- Errors in configuration file values --\n"
77
+ errors.each do |error|
78
+ message += "Unacceptable value for #{error.option_name}. Acceptable values include: #{error.option_values}\n"
79
+ end
80
+
81
+ when :cannot_get_headers
82
+ message += "\n-- Error Retrieving Field Headers --\n"
83
+ message += "We cannot retrieve the column headers from metadata spreadsheet on github,\nwhich define the fields for the metadata below.\nEither the connection to github is failing, \nor the metadata spreadsheet on this branch is not properly formatted.\n"
84
+
85
+ when :bad_header
86
+ message = "\n-- Error interpreting column header(s) --\n"
87
+ message += "We cannot interpret all of the headers from your metadata spreadsheet. \nSpecifically, the following headers did not make sense to us:\n"
88
+ message += errors.map{|error| error.field}.join(", ")+"\n"
89
+
90
+ when :cannot_retrieve_label
91
+ message = "\n-- Errors Retrieving Remote Labels --\n"
92
+ urls = errors.map{|error| error.url}.uniq
93
+ if urls.count < MAX_ERROR
94
+ urls.each do |url|
95
+ url_errors = errors.select{|er| er.url == url}
96
+ message += "Error retrieving label for remote url #{url}. \nThis url appears in #{url_errors.count} instances in the spreadsheet.\n"
97
+ message += "The affected rows are listed here:\n"
98
+ message += url_errors.map{|er| er.row_number}.compact.join('\n')+"\n"
99
+ end
100
+ else
101
+ message += "There were #{urls.count} different URLs in the spreadsheet that we couldn't retrieve labels for,\n making a total of #{errors.count} url related errors.\n These are too many to list, but an example is #{errors.first.url}\n in row #{errors.first.row_number}.\n"
102
+ end
103
+
104
+ when :cannot_retrieve_url
105
+ message = "\n-- Errors Retrieving Remote URLs --\n"
106
+ urls = errors.map{|error| error.url}.uniq
107
+ if urls.count < MAX_ERROR
108
+ urls.each do |url|
109
+ url_errors = errors.select{|er| er.url == url}
110
+ message += "Error retrieving URL for remote authority term #{url}. \nThis term appears in #{url_errors.count} instances in the spreadsheet.\n"
111
+ message += "The affected rows are listed here:\n"
112
+ message += url_errors.map{|er| er.row_number}.compact.join('\n')+"\n"
113
+ end
114
+ else
115
+ message += "There were #{urls.count} different controlled vocab terms in the spreadsheet that we couldn't retrieve or create URLs for,\n making a total of #{errors.count} controlled term related errors.\n These are too many to list, but an example is #{errors.first.url}\n in row #{errors.first.row_number}.\n"
116
+ end
117
+
118
+ when :bad_object_reference
119
+ message = "\n-- Error: bad object reference --\m"
120
+ message += "We enountered #{errors.count} problems resolving object references.\n"
121
+ if errors.count < MAX_ERROR
122
+ message += "The row numbers with problems were:\n"
123
+ message += errors.map{|er| "row number #{er.row_number} references the object #{er.object_id}"}.join("\n")
124
+ else
125
+ message += "For example, row number #{errors.first.row_number} references an object identified by #{errors.first.object_id}, which we cannot find."
126
+ end
127
+
128
+ when :cannot_find_file
129
+ message = "\n-- Missing File Errors --\n "
130
+ message += "We couldn't find the files listed on #{errors.count} rows.\n"
131
+ if errors.count < MAX_ERROR
132
+ message += "Missing filenames:\n"
133
+ message += errors.map{|er| er.file}.join("\n")
134
+ else
135
+ message += "An example of a missing filename is: #{errors.first.file}\n"
136
+ end
137
+
138
+ end
139
+ return message
140
+ end
141
+ end