Skip to content

Commit 58b5b07

Browse files
committed
Implement max csv file size check & wrap import logic within transaction to prevent partially imported dataset
1 parent b68c763 commit 58b5b07

File tree

2 files changed

+55
-42
lines changed

2 files changed

+55
-42
lines changed

app/api/tutorials_api.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,17 @@ class TutorialsApi < Grape::API
110110
end
111111
post '/csv/tutorials/upload' do
112112
unless authorise? current_user, User, :upload_csv
113-
error!({ error: "Not authorised to upload CSV of students of tutorials" }, 403)
113+
error!({ error: 'Not authorised to upload CSV of students of tutorials' }, 403)
114114
end
115+
115116
if params[:file].blank?
116117
error!({ error: 'No file uploaded' }, 403)
117118
end
119+
120+
if params[:file][:tempfile].size > 5.megabytes
121+
error!({ error: 'CSV file size exceeds the 5MB limit' }, 413)
122+
end
123+
118124
path = params[:file][:tempfile].path
119125
Tutorial.import_from_csv(File.new(path))
120126
end

app/models/tutorial.rb

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -89,53 +89,60 @@ def self.import_from_csv(file)
8989
errors = []
9090
ignored = []
9191
data = FileHelper.read_file_to_str(file).gsub('\\n', "\n")
92-
CSV.parse(data,
93-
headers: true,
94-
header_converters: [->(i) { i.nil? ? '' : i }, :downcase, ->(hdr) { hdr.strip unless hdr.nil? }],
95-
converters: [->(body) { body.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '') unless body.nil? }]).each do |row|
96-
missing = missing_headers(row, csv_columns)
97-
if missing.count > 0
98-
errors << { row: row, message: "Missing headers: #{missing.join(', ')}" }
99-
next
100-
end
10192

102-
tutorial_code = row['code'].strip unless row['code'].nil?
103-
abbreviation = row['abbreviation'].strip unless row['abbreviation'].nil?
104-
unit_id = row['unit_id'].strip unless row['unit_id'].nil?
105-
user_id = row['tutor_id'].strip unless row['tutor_id'].nil?
106-
tutorial_stream_name = row['tutorial_stream'].strip unless row['tutorial_stream'].nil?
93+
ActiveRecord::Base.transaction do
94+
CSV.parse(data,
95+
headers: true,
96+
header_converters: [->(i) { i.nil? ? '' : i }, :downcase, ->(hdr) { hdr.strip unless hdr.nil? }],
97+
converters: [->(body) { body.encode!('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '') unless body.nil? }]).each do |row|
98+
missing = missing_headers(row, csv_columns)
99+
if missing.count > 0
100+
errors << { row: row, message: "Missing headers: #{missing.join(', ')}" }
101+
raise StandardError, "Critical import error: missing headers"
102+
end
107103

108-
# find unit role using tutor's user_id and unit_id
109-
unit_role = UnitRole.find_by(user_id: user_id, unit_id: unit_id)
104+
tutorial_code = row['code'].strip unless row['code'].nil?
105+
abbreviation = row['abbreviation'].strip unless row['abbreviation'].nil?
106+
unit_id = row['unit_id'].strip unless row['unit_id'].nil?
107+
user_id = row['tutor_id'].strip unless row['tutor_id'].nil?
108+
tutorial_stream_name = row['tutorial_stream'].strip unless row['tutorial_stream'].nil?
110109

111-
if unit_role.nil?
112-
errors << { row: row, message: "Tutor with user_id (#{user_id}) not found in unit roles" }
113-
next
114-
end
110+
# find unit role using tutor's user_id and unit_id
111+
unit_role = UnitRole.find_by(user_id: user_id, unit_id: unit_id)
112+
113+
if unit_role.nil?
114+
errors << { row: row, message: "Tutor with user_id (#{user_id}) not found in unit roles" }
115+
raise StandardError, 'Critical import error: missing unit role'
116+
end
117+
118+
# if tutorial is found set it, otherwise leave it blank
119+
tutorial_stream = TutorialStream.find_by(name: tutorial_stream_name) if tutorial_stream_name.present?
115120

116-
# if tutorial is found set it, otherwise leave it blank
117-
tutorial_stream = TutorialStream.find_by(name: tutorial_stream_name) if tutorial_stream_name.present?
118-
119-
# handle missing tutorial stream
120-
tutorial_stream = nil if tutorial_stream_name.blank?
121-
122-
# create a new tutorial
123-
tutorial = Tutorial.new(
124-
unit_id: unit_id,
125-
code: tutorial_code,
126-
abbreviation: abbreviation,
127-
unit_role_id: unit_role.id,
128-
tutorial_stream_id: tutorial_stream&.id
129-
)
130-
131-
if tutorial.save
132-
success << { row: row, message: "Created tutorial #{abbreviation} #{unit_id}" }
133-
else
134-
errors << {row: row, message: "Failed to create tutorial #{abbreviation} #{unit_id}" }
121+
# handle missing tutorial stream
122+
tutorial_stream = nil if tutorial_stream_name.blank?
123+
124+
# create a new tutorial
125+
tutorial = Tutorial.new(
126+
unit_id: unit_id,
127+
code: tutorial_code,
128+
abbreviation: abbreviation,
129+
unit_role_id: unit_role.id,
130+
tutorial_stream_id: tutorial_stream&.id
131+
)
132+
133+
if tutorial.save
134+
success << { row: row, message: "Created tutorial #{abbreviation} #{unit_id}" }
135+
else
136+
errors << { row: row, message: "Failed to create tutorial #{abbreviation} #{unit_id}" }
137+
raise StandardError, 'Critical import error: failed to save tutorial'
138+
end
139+
rescue StandardError => e
140+
raise ActiveRecord::Rollback
135141
end
136-
rescue StandardError => e
137-
errors << { row: row, message: e.message }
142+
143+
raise ActiveRecord::Rollback unless errors.empty?
138144
end
145+
139146
{
140147
success: success,
141148
ignored: ignored,

0 commit comments

Comments
 (0)