I have altered my code to prevent an error when zipping a number of files that have the same file name. I want to change it so that duplicate file names are incremented.
For example if you had three files called file.txt, then I want them to be file.txt, file_2.txt and file_3.txt
def zip(file_name)
files_added = []
ZipRuby::Archive.open(file_name, ZipRuby::CREATE) do |archive|
file_associations.sort_by { |fa| fa.created_at }.each do |fa|
attachment_file_name = fa.attachment_file_name
binding.pry
if files_added.include?(attachment_file_name)
n = 2
ext = File.extname(attachment_file_name)
base = File.basename(attachment_file_name, ext)
new_name = "#{base}_#{n}#{ext}"
while files_added.include? new_name
n += 1
new_name = "#{base}_#{n}#{ext}"
end
attachment_file_name = new_name
end
archive.add_buffer(attachment_file_name, fa.attachment.read)
files_added << attachment_file_name
end
end
end
The code above works but I am new to Ruby/Rails and it doesn't feel very Rubyist. Can anyone give any pointers for a refactor.
Related
I was handed a project from someone else, it's in Ruby On Rails, which I know VERY LITTLE. Basically, there is an EXPORT button, that the user clicks to send data to a CSV. I am tasked with sending this data to the view to be seen in HTML. (Thinking I could use dataTables). I have tried following examples, such as:
#example = StudentGroup.where(survey_id: #survey.id).order("groupNum")
and then using <%= #example %> in the view just to see the data and I get nothing. (Also extremely new to MySQL). I'll post the method, if ANYONE can help me, I'd very much appreciate it.
def download_results
if (user_signed_in?)
else
redirect_to new_user_session_path
end
#survey = Survey.find(params[:survey_to_view])
filename = #survey.name + " - " + Date.today.to_formatted_s(:short)
require "csv"
CSV.open(#survey.name+".csv", "wb") do |csv|
csv << [filename]
StudentGroup.where(survey_id: #survey.id).order("groupNum")
csv << []
csv << ["Summarized Results"]
csv << ["UCA","Group Number","Criteria 1","Criteria 2","Criteria 3","Criteria 4","Criteria 5","Criteria 6","Criteria 7","Criteria 8","Overall Team Contribution","Average(Would Work With Again)","Average(C1..C8)","Overall Team Contribution MINUS Average(C1..C9)"]
questions = #survey.questions
numQuestions = 0
questions.each do |q|
if(q.question_type != 2 && q.question_type != 4)
numQuestions = numQuestions+1
end
end
groups.each do |g|
answersCount = Answer.where(student_group_id: g.id).count
if(answersCount == numQuestions && answersCount != 0)
othersInGroup = StudentGroup.where(groupNum: g.groupNum, survey_id: #survey.id).order("groupNum")
size = othersInGroup.count-1
arr = []
criteria = SurveyQuestionDatum.where("number > 24 AND number < 35")
multiAvg = 0
teamCont = 0
criteria.each do |c|
avg = 0
othersInGroup.each do |o|
a = Answer.where(survey_question_datum_id: c.id, student_group_id: o.id).first
if(o.uca != g.uca)
if(a.nil?)
size = size-1
else
avg = avg + a.answer[g.uca].to_i
end
end
end
avg = avg.to_f/size
if(c.number == 33)
teamCont = avg
end
if(c.number < 33)
multiAvg = multiAvg+avg
end
arr << avg
end
multiAvg = multiAvg.to_f/8
arr << multiAvg
arr << teamCont-multiAvg
arr.insert(0,g.uca, g.groupNum)
csv << arr
end
end
csv << []
csv << []
csv << ["Raw Student Answers"]
groups = StudentGroup.where(survey_id: #survey.id).order("groupNum")
size = groups.count
csv << ["UCA", "F-Number", "Group Number"]
groups.each do |g|
answersCount = Answer.where(student_group_id: g.id).count
if(answersCount == numQuestions && answersCount != 0)
othersInGroup = StudentGroup.where(groupNum: g.groupNum, survey_id: #survey.id).order("groupNum")
csv << []
csv << [g.uca, g.FNum, g.groupNum]
answers = Answer.where(student_group_id: g.id)
csv << ["Question Number", "Question", "Answer"]
answers.each do |a|
datum = a.survey_question_datum
question = datum.question
#question_types = {"0" => "short", "1" => "paragraph",
#2" => "title", "3" => "fivept", "4" => "fixed",
#5" =>"ranking", "6"=>"tenpoints","7"=>"hundredpoints"}
ansText = ""
if(question.question_type == 0)
ansText = a.answer
elsif (question.question_type == 1)
if(question.rule == 'perMember')
othersInGroup.each do |o|
ansText = ansText+"#{o.uca},#{a.answer[o.uca]},"
end
elsif(question.rule == 'default')
ansText = a.answer
end
else (question.question_type == 3)
othersInGroup.each do |o|
ansText = ansText+"#{o.uca},#{a.answer[o.uca]},"
end
end
ansText = ansText.chomp(',')
ansText = ansText.split(',')
ansText.insert(0,datum.number,question.question_text)
csv << ansText
end
end
end
end
send_file(#survey.name+".csv", :filename => filename+".csv")
end
You need a new controller action. Take a look at http://guides.rubyonrails.org/layouts_and_rendering.html
Create an index (or show, or whatever you want to call it, maybe example) action. Make sure it is in your routes.
http://guides.rubyonrails.org/getting_started.html#adding-a-route-for-comments
do not use the download_results code.
set your #example variable the way you were trying to do.
create a view for your index action
add the data to your index view.
If you put code in your download_results method (action) it will never get rendered because of the send_file method call.
Did you create a brand new controller / action / view? Did you use generators? Have you really practiced doing this setup exactly the way the examples, videos, tutorials say to do it? If you have, you have seen how all the pieces (models, controllers, actions, views) come together. You should have seen how render statements come into play. Do that, exactly as the tutorials say to do it and you will get the idea.
If you want to use the same content that the download action uses, refactor the code to extract a method that is used both actions.
This is related to respond_to part, check the docs.
send_file(#survey.name+".csv", :filename => filename+".csv")
Your code above simply means you click the button, the controller will respond you with a csv file. So, if you want a html, the controller should be able to respond to html as well.
I have a model that has a method that looks through the filesystem starting at a particular location for files that match a particular regex. This is executed in an after_save callback. I'm not sure how to test this using Rspec and FactoryGirl. I'm not sure how to use something like FakeFS with this because the method is in the model, not the test or the controller. I specify the location to start in my FactoryGirl factory, so I could change that to a fake directory created by the test in a set up clause? I could mock the directory? I think there are probably several different ways I could do this, but which makes the most sense?
Thanks!
def ensure_files_up_to_date
files = find_assembly_files
add_files = check_add_assembly_files(files)
errors = add_assembly_files(add_files)
if errors.size > 0 then
return errors
end
update_files = check_update_assembly_files(files)
errors = update_assembly_files(update_files)
if errors.size > 0 then
return errors
else
return []
end
end
def find_assembly_files
start_dir = self.location
files = Hash.new
if ! File.directory? start_dir then
errors.add(:location, "Directory #{start_dir} does not exist on the system.")
abort("Directory #{start_dir} does not exist on the system for #{self.inspect}")
end
Find.find(start_dir) do |path|
filename = File.basename(path).split("/").last
FILE_TYPES.each { |filepart, filehash|
type = filehash["type"]
vendor = filehash["vendor"]
if filename.match(filepart) then
files[type] = Hash.new
files[type]["path"] = path
files[type]["vendor"] = vendor
end
}
end
return files
end
def check_add_assembly_files(files=self.find_assembly_files)
add = Hash.new
files.each do |file_type, file_hash|
# returns an array
file_path = file_hash["path"]
file_vendor = file_hash["vendor"]
filename = File.basename(file_path)
af = AssemblyFile.where(:name => filename)
if af.size == 0 then
add[file_path] = Hash.new
add[file_path]["type"] = file_type
add[file_path]["vendor"] = file_vendor
end
end
if add.size == 0 then
logger.error("check_add_assembly_files did not find any files to add")
return []
end
return add
end
def check_update_assembly_files(files=self.find_assembly_files)
update = Hash.new
files.each do |file_type, file_hash|
file_path = file_hash["path"]
file_vendor = file_hash["vendor"]
# returns an array
filename = File.basename(file_path)
af = AssemblyFile.find_by_name(filename)
if !af.nil? then
if af.location != file_path or af.file_type != file_type then
update[af.id] = Hash.new
update[af.id]['path'] = file_path
update[af.id]['type'] = file_type
update[af.id]['vendor'] = file_vendor
end
end
end
return update
end
def add_assembly_files(files=self.check_add_assembly_files)
if files.size == 0 then
logger.error("add_assembly_files didn't get any results from check_add_assembly_files")
return []
end
asm_file_errors = Array.new
files.each do |file_path, file_hash|
file_type = file_hash["type"]
file_vendor = file_hash["vendor"]
logger.debug "file type is #{file_type} and path is #{file_path}"
logger.debug FileType.find_by_type_name(file_type)
file_type_id = FileType.find_by_type_name(file_type).id
header = file_header(file_path, file_vendor)
if file_vendor == "TBA" then
check = check_tba_header(header, file_type, file_path)
software = header[TBA_SOFTWARE_PROGRAM]
software_version = header[TBA_SOFTWARE_VERSION]
elsif file_vendor == "TBB" then
check = check_tbb_header(header, file_type, file_path)
if file_type == "TBB-ANNOTATION" then
software = header[TBB_SOURCE]
else
software = "Unified"
end
software_version = "UNKNOWN"
end
if check == 0 then
logger.error("skipping file #{file_path} because it contains incorrect values for this filetype")
asm_file_errors.push("#{file_path} cannot be added to assembly because it contains incorrect values for this filetype")
next
end
if file_vendor == "TBA" then
xml = header.to_xml(:root => "assembly-file")
elsif file_vendor == "TBB" then
xml = header.to_xml
else
xml = ''
end
filename = File.basename(file_path)
if filename.match(/~$/) then
logger.error("Skipping a file with a tilda when adding assembly files. filename #{filename}")
next
end
assembly_file = AssemblyFile.new(
:assembly_id => self.id,
:file_type_id => file_type_id,
:name => filename,
:location => file_path,
:file_date => creation_time(file_path),
:software => software,
:software_version => software_version,
:current => 1,
:metadata => xml
)
assembly_file.save! # exclamation point forces it to raise an error if the save fails
end # end files.each
return asm_file_errors
end
Quick answer: you can stub out model methods like any others. Either stub a specific instance of a model, and then stub find or whatever to return that, or stub out any_instance to if you don't want to worry about which model is involved. Something like:
it "does something" do
foo = Foo.create! some_attributes
foo.should_receive(:some_method).and_return(whatever)
Foo.stub(:find).and_return(foo)
end
The real answer is that your code is too complicated to test effectively. Your models should not even know that a filesystem exists. That behavior should be encapsulated in other classes, which you can test independently. Your model's after_save can then just call a single method on that class, and testing whether or not that single method gets called will be a lot easier.
Your methods are also very difficult to test, because they are trying to do too much. All that conditional logic and external dependencies means you'll have to do a whole lot of mocking to get to the various bits you might want to test.
This is a big topic and a good answer is well beyond the scope of this answer. Start with the Wikipedia article on SOLID and read from there for some of the reasoning behind separating concerns into individual classes and using tiny, composed methods. To give you a ballpark idea, a method with more than one branch or more than 10 lines of code is too big; a class that is more than about 100 lines of code is too big.
This is my method for uploading files:
name = file.original_filename
directory = "images/"
path = File.join(directory, name)
File.open(path, "wb") { |f| f.write(file.read) }
I need to rename the uploaded file - I want to give it a unique name. But how can I obtain the file name and the extension?
One way on how to rename the file is from the filename remove the extension (.jpg - remove last 4 chars), rename the file and then merge the name+extension.
But this is a bit dirty way. Is there something cleaner and more elegant?
A 'little' late but I put this answer for those who are still searching and get here.
photo = params[:photo]
name = photo.original_filename
directory = "public/uploads/photos"
path = File.join(directory, name)
uniq_name = (0...10).map { (65 + rand(26)).chr }.join
time_footprint = Time.now.to_formatted_s(:number)
File.open(path, "wb") do |file|
file.write(photo.read)
#uniq_path = File.join(directory, uniq_name + time_footprint + File.extname(file))
File.rename(file, #uniq_path)
end
I take the random string generation from How to generate a random string in Ruby. And set #uniq_path to use it on a create function after.
What about doing this?
File.rename(file, folder_path + "/" + new_name + File.extname(file))
For example, calling this script on the same folder of the file:
new_name = "TESTING"
File.open("test.txt") do |file|
File.rename(file, new_name + File.extname(file))
end
Will rename the file to: TESTING.txt
I got confused about the iteration in ruby.In the following code I wrote, I expected that the two paths print out should be the same. But actually they are not. Seems the path was changed in the for loop.
Anything wrong in my code? Thanks
def one_step_search(dest,paths)
direction = %w(north east south west)
new_paths = []
paths.map do |path|
print "original path is: "
print_path path
curr_room = path.last
for i in 0..3
new_path = path
if !curr_room.send("exit_#{direction[i]}").nil?
next_room_tag = curr_room.send("exit_#{direction[i]}")[0]
next_room = find_room_by_tag(next_room_tag)
if !new_path.include?(next_room) # don't go back to the room visited before
new_path << next_room
new_paths << new_path
print "new path is: "
print_path path
return new_paths if dest.tag == next_room_tag
end
end
end
end
return new_paths
end
It seems to me that problem is in this line
new_path = path
You may think that new_path and path are different objects but it's not. I'll illustrate by example:
a = "foo"
b = a
puts a.sub!(/f/, '_')
puts a # => "_oo"
puts b # => "_oo"
a and b are references that pointing to one object.
The simpliest solution for you will be to use dup or clone
new_path = path.clone
but actually your code requires good cleaning.
I am new here. I am working on a project with some tests. I have some problems with writing spec for a class. I am done with some simple specs but I have no clue how to write for this one. Any help will be highly appreciated.
My class
Class Writer
def initialize(filepath)
#filepath = RAILS_ROOT + filepath
#xml_document = Nokogiri::XML::Document.new
end
def open
File.open(#filepath,"w") do |f|
#gz = Zlib::GzipWriter.new(f)
#gz.write(%[<?xml version="1.0" encoding="UTF-8"?>\n])
#gz.write(%[<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n])
yield self
#gz.write(%[</urlset>])
#gz.close
end
end
def write_entry_to_xml(entry)
node = Nokogiri::XML::Node.new( "url" , #xml_document )
node["loc"] = entry.loc
node["changefreq"] = entry.changfreq
node["priority"] = entry.priority
node["lastmod"] = entry.lastmod
#gz.write(node.to_xml)
end
end
What I have written so far is as follows
describe "writer" do
before :each do
#time = Time.now
#filepath = RAILS_ROOT + "/public/sitemap/test/sitemap_test.xml.gz"
File.open(#filepath,"w") do |f|
#gz = Zlib::GzipWriter.new(f)
end
#xml_document = Nokogiri::XML::Document.new
#entry = Sitemap::Entry.new("location", "monthly", "0.8", #time)
end
describe "open" do
it "should create a file and write xml entries to it" do
end
end
describe "write_entry_to_xml" do
it "should format and entry to xml node and write it" do
node = Nokogiri::XML::Node.new( "url" , #xml_document )
node["loc"].should == #entry.loc
node["changefreq"].should == #entry.changfreq
node["priority"].should == #entry.priority
node["lastmod"].shoul == #entry.lastmod
end
end
Can anyone help me in writing the complete specs for this class.
Thanks in advance
I don't have time to do all this for you, but here are examples of how I am testing my code:
actual code
it's spec
Notice this: Ropet::Config.expects(:new).returns(config), this can be used for your Nokogiri::XML::Node#new.
My specs use RSpec and Mocha, I like the simplicity of this setup and what can be done with those simple tools.
Edit: rough spec for
def write_entry_to_xml(entry)
node = Nokogiri::XML::Node.new( "url" , #xml_document )
node["loc"] = entry.loc
node["changefreq"] = entry.changfreq
node["priority"] = entry.priority
node["lastmod"] = entry.lastmod
#gz.write(node.to_xml)
end
It could be something like this, though i don't know the purpose of your code.
it 'writes entry to xml' do
content = double('output')
node = double('node'); node.should_receive(:to_xml).and_return(content);
gz = double('gz'); gz.should_receive(:write).with(content)
w = Writer.new("some_path"); w.open
w.instance_variable_set(:#gz, gz) # i'm guessing #gz is assigned after open only?
entry = # i don't know what entry is
Nokogiri::XML::Node.stub(:new).and_return(node)
node.should_receive(:[]).with("loc", entry.loc)
node.should_receive(:[]).with("changefreq", entry.changefreq)
node.should_receive(:[]).with("priority", entry.priority)
node.should_receive(:[]).with("lastmod", entry.lastmod)
w.write_entry_to_xml(entry)
end