Image files should be renamed as [variant-name]-[underscored-option-type].jpg for variants. I have come this far.
Updated Code
Spree::Image.class_eval do
after_save :change_file_name
private
def change_file_name
if self.viewable.kind_of? Spree::Variant
product_name = self.viewable.product.name.downcase.gsub(" ","_")
underscored_option_types = get_underscored_option_types
random_number = rand(10000...1000000)
extension = File.extname(self.attachment_file_name).downcase
attachment_file_name = product_name+"-"+underscored_option_types+"-"+"#{random_number}"+"#{extension}"
self.update_column(:attachment_file_name, attachment_file_name)
end
end
end
This code renames attachment_file_name column only. How to change image's name? Even self.save won't work, considering I escape recursive loop.
I had to rename the files at their respective locations, as there are different styles (version) of images stored. You have to rename each versions of an image at their respective locations. I hope the following code will help someone.cheers :)
Spree::Image.class_eval do
after_save :change_file_name
private
def change_file_name
#skip_change_file_name ||= false
return if #skip_change_file_name
if self.viewable.kind_of? Spree::Variant
product_name = self.viewable.product.name.downcase.gsub(" ","_")
underscored_option_types = get_underscored_option_types
random_number = rand(10000...1000000)
extension = File.extname(self.attachment_file_name).downcase
new_file_name = product_name+"-"+underscored_option_types+"-"+"#{random_number}"+"#{extension}"
(self.attachment.styles.keys+[:original]).each do |style|
FileUtils.move(self.attachment.path(style), File.join(File.dirname(self.attachment.path(style)), new_file_name))
end
self.attachment_file_name = new_file_name
#skip_change_file_name = true
self.save!
end
end
end
Related
I'm trying to generate stats for a character created by a form. The user inputs the name, race, class, alignment, and whether or not the stats will be generated randomly, or prioritized (values being assigned from highest to lowest). The form works flawlessly, as I can see the output in a view.
What I am now trying to do is call a method from a class in /lib in the model that will generate the stats; however, I keep getting the following error (I can't post pictures):
NoMethodError in CharactersController#create
undefined method `[]' for nil:NilClass
Extracted source (around line #14):
12 before_save do
13 generate_stats
14 self.strength = #character_stats[:strength]
15 self.dexterity = #character_stats[:dexterity]
16 self.constitution = #character_stats[:constitution]
17 self.intelligence = #character_stats[:intelligence]
Here is a copy of some of my code:
In controllers\characters_controller.rb
class CharactersController < ApplicationController
def create
#character = Character.new(character_info_params)
#character.name = params[:character][:name].capitalize
#character.alignment = "#{params[:character][:alignment_lr]} #{params[:character][:alignment_ud]}"
if #character.save
redirect_to #character
else
render 'new'
end
end
private
def character_info_params
params.require(:character).permit(:name, :race, :class_, :alignment)
end
end
In models\character.rb
class Character < ActiveRecord::Base
require 'random_stats_generator'
attr_accessor :rand_stat_gen
def generate_stats
if #rand_stat_gen == true
#character_stats_inst = RandomStatGenerator.new
#character_stats = #character_stats_inst.generate
end
end
before_save do
generate_stats
self.strength = #character_stats[:strength]
self.dexterity = #character_stats[:dexterity]
self.constitution = #character_stats[:constitution]
self.intelligence = #character_stats[:intelligence]
self.wisdom = #character_stats[:wisdom]
self.charisma = #character_stats[:charisma]
end
#validation passed this point
end
In initializers\stat_builders.rb
require "./lib/random_stat_generator.rb"
In lib/random_stat_generator.rb
class RandomStatGenerator
def initialize
#strength = :strength
#dexterity = :dexterity
#constitution = :constitution
#intelligence = :intelligence
#wisdom = :wisdom
#charisma = :charisma
#character_stats = HashWithIndifferentAccess.new()
end
def self.generate
roll_stats
end
def roll(stat)
#roll_value_1 = (1 + (rand(6)))
#roll_value_2 = (1 + (rand(6)))
#roll_value_3 = (1 + (rand(6)))
#roll_value_4 = (1 + (rand(6)))
#roll_array = [#roll_value_1,#roll_value_2,#roll_value_3,#roll_value_4]
#roll_array = #roll_array.sort_by {|x| x }
#roll_array = #roll_array.reverse
stat = #roll_array[0] + #roll_array[1] + #roll_array[2]
end
def roll_stats
#strength = roll(#strength)
#dexterity = roll(#dexterity)
#constitution = roll(#constitution)
#intelligence = roll(#intelligence)
#wisdom = roll(#wisdom)
#charisma = roll(#charisma)
#character_stats[:strength] = #strength
#character_stats[:dexterity] = #dexterity
#character_stats[:constitution] = #constitution
#character_stats[:intelligence] = #intelligence
#character_stats[:wisdom] = #wisdom
#character_stats[:charisma] = #charisma
return #character_stats
end
end
To me, it looks like the method isn't returning anything, or isn't being called at all.
I've tried a lot of solutions that I've come across online, none of them working. There may be some things that don't really make sense that are left over from these solutions. I'm only just starting with rails, so I'm still trying to get used to everything.
Thanks a lot for your help.
Ruby has really powerful functions for manipulating both hashes and arrays.
Typing out duplicate assignments like:
self.strength = #character_stats[:strength]
self.dexterity = #character_stats[:dexterity]
self.constitution = #character_stats[:constitution]
Is pretty dull. So instead we can simply rewrite the methods to pass hashes around.
class RandomStatGenerator
# This is just a constant containing all the stats we want to generate.
STATS = [:strength, :dexterity, :constitution, :intelligence, :wisdom, :charisma]
# Create a hash with random roll values for each stat
def self.roll_stats
# This is kind of scary looking but actually just creates an
# hash from an array of keys
Hash[STATS.map {|k| [k, self.roll ] } ]
end
private
def self.roll
# Create an array with 4 elements (nil)
ary = Array.new(4)
# We then replace the nil value with a random value 1-6
ary = ary.map do
(1 + (rand(6)))
end
# sort it and drop the lowest roll. return the sum of all rolls.
ary.sort.drop(1).sum
# a ruby ninja writes it like this
Array.new(4).map { 1 + rand(6) }.sort.drop(1).sum
end
end
Output:
irb(main):032:0> RandomStatGenerator.roll_stats
=> {:strength=>14, :dexterity=>14, :constitution=>14, :intelligence=>13, :wisdom=>10, :charisma=>9}
But if you don't intend to actually create instances of a class, than you should use a module instead.
Rails models can either be created with a hash or you can replace its values with a hash:
Character.new(RandomStatGenerator.roll_stats)
#character.assign_attributes(RandomStatGenerator.roll_stats)
So we can use this in Character#generate_stats:
def generate_stats
assign_attributes(RandomStatGenerator.roll_stats)
end
You should use ActiveModel callbacks with extreme prejudice. It is often quite a challenge to regulate where in your application and when in the model lifetime. Since before_save runs after validations means that any validations like validates_presence_of :constitution will fail.
In your case it might be better to simply do it in the controller or use:
before_validation :generate_stats, if: -> { new_record? && #rand_stat_gen }
I would like to suggest the following organisation fo your library
# Use a module at top level
module RandomStatGenerator
STATS = [:strength, :dexterity, :constitution, :intelligence, :wisdom, :charisma]
# Use a class Stats if you need to but I don't see why...
class Stats
def initialize
RandomStatGenerator::STATS.each do |stat|
# Below line will do #stat = :stat
instance_variable_set("##{stat.to_s}", stat)
#character_stats = HashWithIndifferentAccess.new()
end
def roll_stats
#character_stats = RandomStatGenerator.roll_stats
end
end
module_function
# below lines will be considered as module functions
# => call RandomStatGenerator.function_name
def roll
roll_value_1 = (1 + (rand(6)))
roll_value_2 = (1 + (rand(6)))
roll_value_3 = (1 + (rand(6)))
roll_value_4 = (1 + (rand(6)))
roll_array = [roll_value_1,roll_value_2,roll_value_3,roll_value_4]
roll_array = roll_array.sort_by {|x| x }
roll_array = roll_array.reverse
roll_array[0] + roll_array[1] + roll_array[2]
end
def roll_stats
character_stats = {}
STATS.each do |stat|
character_stats[stat] = RandomStatGenerator.roll
end
return character_stats
end
end
Then in your character.rb
def generate_stats
#character_stats = RandomStatGenerator.roll_stats
end
I'm trying to create a method that deletes files on an S3 instance, but I am getting a AWS::S3::Errors::NoSuchKey: No Such Key error when I try to call .head or .read on an object.
app/models/file_item.rb
def thumbnail
{
exists: thumbnailable?,
small: "http://#{bucket}.s3.amazonaws.com/images/#{id}/small_thumb.png",
large: "http://#{bucket}.s3.amazonaws.com/images/#{id}/large_thumb.png"
}
end
lib/adapters/amazons3/accessor.rb
module Adapters
module AmazonS3
class Accessor
S3_BUCKET = AWS::S3.new.buckets[ENV['AMAZON_BUCKET']]
...
def self.delete_file(thumbnail)
prefix_pattern = %r{http://[MY-S3-HOST]-[a-z]+.s3.amazonaws.com/}
small_path = thumbnail[:small].sub(prefix_pattern, '')
large_path = thumbnail[:large].sub(prefix_pattern, '')
small = S3_BUCKET.objects[small_path]
large = S3_BUCKET.objects[large_path]
binding.pry
S3_BUCKET.objects.delete([small, large])
end
end
end
end
example url1
"http://projectname-staging.s3.amazonaws.com/images/994/small_thumb.png"
example url2
"http://projectname-production.s3.amazonaws.com/images/994/large_thumb.png"
assuming awssdk v1 for ruby.
small = S3_BUCKET.objects[small_path]
does not actually get any objects.
from: https://docs.aws.amazon.com/AWSRubySDK/latest/AWS/S3/Bucket.html
bucket.objects['key'] #=> makes no request, returns an S3Object
bucket.objects.each do |obj|
puts obj.key
end
so you would need to alter your code to something like:
to_delete = []
S3_BUCKET.objects[small_path].each do |obj|
to_delete << obj.key
end
S3_BUCKET.objects[large_path].each do |obj|
to_delete << obj.key
end
S3_BUCKET.objects.delete(to_delete)
just banged out the code, so the idea is there, you might need to correct/polish it a bit
I was able to come of with a kind of different solution thanks to your answer of #Mircea above.
def self.delete_file(thumbnail)
folder = thumbnail[:small].match(/(\d+)(?!.*\d)/)
to_delete = []
S3_BUCKET.objects.with_prefix("images/#{folder}").each do |thumb|
to_delete << thumb.key
end
# binding.pry
S3_BUCKET.objects.delete(to_delete)
end
So I have this code in my index action, would love to move it to a model, just a little confused on how to do it.
Original Code
def index
urls = %w[http://cltampa.com/blogs/potlikker http://cltampa.com/blogs/artbreaker http://cltampa.com/blogs/politicalanimals http://cltampa.com/blogs/earbuds http://cltampa.com/blogs/dailyloaf http://cltampa.com/blogs/bedpost]
#final_images = []
#final_urls = []
urls.each do |url|
blog = Nokogiri::HTML(open(url))
images = blog.xpath('//*[#class="postBody"]/div[1]//img/#src')
images.each do |image|
#final_images << image
end
story_path = blog.xpath('//*[#class="postTitle"]/a/#href')
story_path.each do |path|
#final_urls << path
end
end
end
I tested this code in my model and it works perfectly for one url, just not sure how to integrate all of the urls like the original code.
New Code
Model
class Photocloud < ActiveRecord::Base
attr_reader :url, :data
def initialize(url)
#url = url
end
def data
#data ||= Nokogiri::HTML(open(url))
end
def get_elements(path)
data.xpath(path)
end
end
Controller
def index
#scraper = Photocloud.new('http://cltampa.com/blogs/artbreaker')
#photos = #scraper.get_elements('//*[#class="postBody"]/div[1]//img/#src')
#story_urls = #scraper.get_elements('//*[#class="postBody"]/div[1]//img/#src')
end
My main questions are how would I initialize multiple urls and loop through them like my original code. I have tried different things but feel like I have hit a wall. I need to save them to the database, but would like to get this working first. Any help is greatly appreciated.
Updated Controller - WIP
def index
start_urls = %w[http://cltampa.com/blogs/potlikker
http://cltampa.com/blogs/artbreaker
http://cltampa.com/blogs/politicalanimals
http://cltampa.com/blogs/earbuds
http://cltampa.com/blogs/dailyloaf
http://cltampa.com/blogs/bedpost]
#scraper = Photocloud.new(start_urls)
#images =
#paths =
end
Need some help with this part...
It seems that you don't persist scraped images and paths to the database so Photocloud doesn't need to inherit from ActiveRecord::Base - it can be just a plain old ruby object (PORO):
class Photocloud
attr_reader :start_urls
attr_accessor :images, :paths
def initialize(start_urls)
#start_urls = start_urls
#images = []
#paths = []
end
def scrape
start_urls.each do |start_url|
blog = Nokogiri::HTML(open(url))
scrape_images(blog)
scrape_paths(blog)
end
end
private
def scrape_images(blog)
images = blog.xpath('//*[#class="postBody"]/div[1]//img/#src')
images.each do |image|
images << image
end
end
def scrape_paths(blog)
story_path = blog.xpath('//*[#class="postTitle"]/a/#href')
story_path.each do |path|
paths << path
end
end
end
In controller:
scraper = Photocloud.new(start_urls)
scraper.scrape
#images = scraper.images
#paths = scraper.paths
This is only one of the possibilities how you could structure code, of course.
I have a concern allowing me to give the back end user the ability to sort elements. I use it for a few different elements. The rails community seems to be pretty vocal against concern and callbacks, i'd like to have a few pointers on how to better model the following code :
require 'active_support/concern'
module Rankable
extend ActiveSupport::Concern
included do
validates :row_order, :presence => true
scope :next_rank, lambda { |rank| where('row_order > ?',rank).order("row_order asc").limit(1)}
scope :previous_rank, lambda { |rank| where('row_order < ?',rank).order("row_order desc").limit(1)}
scope :bigger_rank, order("row_order desc").limit('1')
before_validation :assign_rank
end
def invert(target)
a = self.row_order
b = target.row_order
self.row_order = target.row_order
target.row_order = a
if self.save
if target.save
true
else
self.row_order = a
self.save
false
end
else
false
end
end
def increase_rank
return false unless self.next_rank.first && self.invert(self.next_rank.first)
end
def decrease_rank
return false unless self.previous_rank.first && self.invert(self.previous_rank.first)
end
private
def assign_default_rank
if !self.row_order
if self.class.bigger_rank.first
self.row_order = self.class.bigger_rank.first.row_order + 1
else
self.row_order=0
end
end
end
end
I think a Concern is a good choice for what you are trying to accomplish (particularly with validations and scopes because ActiveRecord does those two very well). However, if you did want to move things out of the Concern, apart from validations and scopes, here is a possibility. Just looking at the code it seems like you have a concept of rank which is represented by an integer but can become it's own object:
class Rank
def initialize(rankable)
#rankable = rankable
#klass = rankable.class
end
def number
#rankable.row_order
end
def increase
next_rank ? RankableInversionService.call(#rankable, next_rank) : false
end
def decrease
previous_rank ? RankableInversionService.call(#rankable, previous_rank) : false
end
private
def next_rank
#next_rank ||= #klass.next_rank.first
end
def previous_rank
#previous_rank ||= #klass.previous_rank.first
end
end
To extract out the #invert method we could create a RankableInversionService (referenced above):
class RankableInversionService
def self.call(rankable, other)
new(rankable, other).call
end
def initialize(rankable, other)
#rankable = rankable
#other = other
#original_rankable_rank = rankable.rank
#original_other_rank = other.rank
end
def call
#rankable.rank = #other.rank
#other.rank = #rankable.rank
if #rankable.save && #other.save
true
else
#rankable.rank = #original_rankable_rank
#other.rank = #original_other_rank
#rankable.save
#other.save
false
end
end
end
To extract out the callback you could have a RankableUpdateService which will assign the default rank prior to saving the object:
class RankableUpdateService
def self.call(rankable)
new(rankable).call
end
def initialize(rankable)
#rankable = rankable
#klass = rankable.class
end
def call
#rankable.rank = bigger_rank unless #rankable.ranked?
#rankable.save
end
private
def bigger_rank
#bigger_rank ||= #klass.bigger_rank.first.try(:rank)
end
end
Now you concern becomes:
module Rankable
extend ActiveSupport::Concern
included do
# validations
# scopes
end
def rank
#rank ||= Rank.new(self)
end
def rank=(rank)
self.row_order = rank.number; #rank = rank
end
def ranked?
rank.number.present?
end
end
I'm sure there are issues with this code if you use it as is, but you get the concept. Overall I think the only thing that might be good to do here is extracting out a Rank object, other than that it might be too much complexity that the concern encapsulates pretty nicely.
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.