`Case/when` statement works some of the time - ruby-on-rails

Ok I am creating a gem that is supposed to find tags #, #, or $ in user's posts. I am using a case when statement and it seems to work, only sometimes. For example I will have a string like #you and that works, but #cool does not work unless I add #cool #you. It seems the other when statements only work if the first when statement is true. The REGEX is just so it knows what to look for and I know those do work.
REGEXS = [Supertag::Tag::USERTAG_REGEX, Supertag::Tag::HASHTAG_REGEX, Supertag::Tag::MONEYTAG_REGEX]
def linkify_tags(taggable_content)
text = taggable_content.to_s
REGEXS.each do
case text
when text = text.gsub(Supertag::Tag::USERTAG_REGEX) {link_to($&, usertag_path($2), class: 'tag')}
when text = text.gsub(Supertag::Tag::HASHTAG_REGEX) {link_to($&, hashtag_path($2), class: 'tag')}
when text = text.gsub(Supertag::Tag::MONEYTAG_REGEX) {link_to($&, moneytag_path($2), class: 'tag')}
end
end
text.html_safe
end

For some reason you iterate over REGEXS, ignore the item in the iteration, then hard-code them again... you actually do text.gsub(Supertag::Tag::USERTAG_REGEX) ... 3 times - once for each REGEX in your list.
Also, you misuse the case when construct, I suggest you read more about it
You should either drop the each entirely, and use only the explicit constants, or refactor you code to work dynamically, maybe something like:
REGEXS = [[Supertag::Tag::USERTAG_REGEX, :usertag_path],
[Supertag::Tag::HASHTAG_REGEX, :hashtag_path],
[Supertag::Tag::MONEYTAG_REGEX, :moneytag_path]]
def linkify_tags(taggable_content)
text = taggable_content.to_s
REGEXS.each do |regex, path|
text = text.gsub(regex) {link_to($&, send(path, $2), class: 'tag')}
end
text.html_safe
end

Related

Ruby switch words for hyperlinks without changing the chars

I have two different models: post (it has a content) and keywords (it has the word and the link). I am trying to make a function which would switch words in post content with the same keywords and its link (so it would work as hyperlink) For examples there is a keyword 'Hello' with some link on it and word 'hello', I want 'hello' in post.content to become a hyperlink with link from 'Hello' in keywords.
Here is my function:
def execute
#post = Post.find(params[:post_id])
all_keys = Keyword.all.pluck(:key, :link)
all_keys = all_keys.map.to_h
all_keys = all_keys.transform_keys(&:downcase)
new_content = #post.content.to_s
new_content_downcase = new_content.downcase
all_keys.map { |key, link| new_content_downcase.gsub!(key, "<a href='#{link}'>#{key}</a>") }
#post.content = new_content_downcase
#post.save!
end
Function is easy: I made a hash {key: 'link'} and have #post.content, then I downcase hash keys and #post.content and switch the words in post content with key from hash and link (so it would look like hyperlink).
Everything works fine but the problem is that it switch words in #post.content to lowercase (Hello --> hello). Is there any way to switch compare new_content and new_content_downcase, save the original word AND hyperlink on it?
Just don't downcase the post's content, that's it :) You could use gsub! with the block to make things concise, smth. like the following:
def execute
#post = Post.find(params[:post_id])
keys = Keyword.pluck(:key, :link).to_h.transform_keys(&:downcase)
#post.content.gsub!(/\w+/) do |word|
# We downcase each word when we check for the links presence...
url = keys[word.downcase]
# ... but not when we do replacements.
url ? "<a href='#{url}'>#{word}</a>" : word
end
#post.save!
end
So, your output is all lower case because you've applied #downcase to both your list of keywords and your content. And I assume you did that because you're doing a literal match between the keyword and the content string in your gsub.
One solution is to use a case-insensitive regex instead, :
all_keys.map { |key, link|
#post.content.gsub!(/(#{key})/i, "<a href='#{link}'>\1</a>")
}
Here, I've ignored the downcase and just used #post.content directly (I assume that it's a string so the to_s is redundant).
Then, in the gsub, I replaced the key direct match with a regex. This uses brackets to capture the term that's found for use in the replace term, so that you retain the capitalisation of the source rather than that of the stored keyword. The \1 in the replacement string is how that stored result from the regex gets used.
Fingers crossed that gets you working!
===Edit===
Here's an attempt at doing this properly, updating the entire method. (I'd also not escaped the \1 above, which it needs because it's in double quotes. Sorry about that!)
def export
#post = Post.find(params[:post_id])
_content = #post.content
Keyword.pluck(:key, :link).to_h.each { |_key, _link|
_content.gsub!(/(#{_key})/i, "<a href='#{_link}'>\\1</a>")
}
#post.update(content: _content)
end
Don't add key after \1, as you mention in a comment - the \1 should automatically be replaced with whatever was found by the regex (i.e. the value of key regardless of case).
Also, you shouldn't need to downcase your Keyword entries in any case: the time to do that is when they're created, so you only have to do it once.

prompt option not working with select_tag

If there is value in marital_status then prompt should not be displayed but in my case it's displaying. My code is mentioned below. Please help.
= select_tag( 'request[marital_status]',
options_for_select(marital_status_options,
#employee.marital_status.try(:upcase)), prompt: "Select Marital Status", id: 'employee_marital_status', disabled: #request.submitted?)
In employee_helper.rb
def marital_status_options
Employee::MaritalStatus::ALL.zip(Employee::MaritalStatus::ALL)
end
In employee model
module MaritalStatus
MARRIED = 'MARRIED'
SINGLE = 'SINGLE'
DIVORCED = 'DIVORCED'
ALL = [MARRIED, SINGLE, DIVORCED]
end
You're very close. The problem here likely stems from your marital_status_options method: this will simply return DIVORCED as it evaluates to the last line due to your assignment.
Therefore, you might find the value is selected if your instance contains 'DIVORCED', though not either of the other values; your instance's value needs to match one of these for it to be selected instead of the prompt.
You likely want to change this:
def marital_status_options
MARRIED = 'MARRIED' # this will be evaluated first
SINGLE = 'SINGLE' # then this
DIVORCED = 'DIVORCED' # finally, this will be evaluated and returned as 'DIVORCED'
end
To an array, either:
def marital_status_options
['MARRIED', 'SINGLE', 'DIVORCED']
end
Or, to present the options as lowercase but keep uppercase values in the db:
def marital_status_options
[['Married', 'MARRIED'], ['Single', 'SINGLE'], ['Divorced', 'DIVORCED']]
end
Take a look at the docs on options_for_select and you'll see who they can be setup.
Further down the line, you might want to consider switching to enums - these are very handy for managing selections such as these, and auto generate methods such as Employee.married, employee.divorced?, and so forth.
As someone else has mentioned, it's best practice to store data such as this in the relevant model, though I'd argue these should be stored as a constant as they won't be changing. So one of the following:
# employee.rb
MARITAL_STATUSES = ['MARRIED', 'SINGLE', 'DIVORCED'].freeze
# or
MARITAL_STATUSES = [['Married', 'MARRIED'], ['Single', 'SINGLE'], ['Divorced', 'DIVORCED']].freeze
= select_tag('request[marital_status]',
options_for_select(Employee::MARITAL_STATUSES,
#employee.marital_status.try(:upcase)),
prompt: "Select Marital Status",
id: 'employee_marital_status',
disabled: #request.submitted?)
Hope that helps - let me know if you've any questions or need anything else.
The format and usage is correct. Kindly verify if #employee.marital_status.try(:upcase) exactly matches one of the marital_status_options provided here.
That looks like a probable case for such a behaviour.
Also, the first argument expected in select_tag needs to in an appropriate format, in this case, an array of strings.
Hence, your method marital_status_options should return an array of options to be used for dropdown.
def marital_status_options
['MARRIED', 'SINGLE', 'DIVORCED']
end
= select_tag "request[marital_status]", options_for_select(Employee.marital_status_options,
#employee.marital_status.try(:upcase)), :include_blank => '--Select Marital Status--', id: id: 'employee_marital_status', disabled: #request.submitted?
It's a good practice to define marital_status_options(Business logic) inside model: -
Assuming that it's Employee model
def self.marital_status_options
[
["MARRIED","MARRIED"],
["SINGLE","SINGLE"],
["DIVORCED", "DIVORCED"]
]
end
Reason that it's not selecting your default marital_status is because if #employee.marital_status.try(:upcase) will not match any of the marital_status_options, it will show your prompt option, so check it carefully that if #employee.marital_status.try(:upcase) matches any of the given options of select tag's option.

Multiple word search in one field

I have search functionality with this code in the 'search.rb' file:
votsphonebooks = votsphonebooks.where("address like ?", "%#{address}%") if address.present?
There are multiple fields, this is just one of them.
How can I successfully change this line into something like a map to include multiple words.
Eg. If they type in '123 Fake St' - it will look for exactly that, but I want it to search for '123', 'Fake', 'St'.
First thing you should do is split the address by spaces:
addresses = params[:address].split(" ")
Then what you need is a OR query, you could do it by using ARel.
t = VotsPhoneBook.arel_table # The class name is my guess
arel_query = addresses.reduce(nil) do |q, address|
q.nil? ? t[:address].matches("%#{address}%") : q.or(t[:address].matches("%#{address}%"))
end
results = Post.where(arel_query)
Try using REGEXP instead of LIKE:
address_arr = address.split(" ")
votsphonebooks = votsphonebooks.where('address REGEXP ?',address_arr.join('|')) unless address_arr.blank?

how to create dynamic variables in rails function?

I have this code that's working:
case index
when "Books"
#reading_img = res.items.first.get_hash('MediumImage')["URL"] # don't show an image
#reading_link = create_amz_url(search, asin)
tempy = #nowreading.content.match(/#nowreading.*/).to_s.gsub("#nowreading",'') # strips away the #nowreading tag
#nowreading.content = tempy.match(/#{search}.*/).to_s.gsub("#{search}", #reading_link)
# replaces the book title (passed in as variable 'search') with the URL'ed title
when "Music"
#listening_img = res.items.first.get_hash('MediumImage')["URL"] # don't show an image
#listening_link = create_amz_url(search, asin)
tempy = #nowlistening.content.match(/#nowlistening.*/).to_s.gsub("#nowlistening",'') # strips away the #nowreading tag
#nowlistening.content = tempy.match(/#{search}.*/).to_s.gsub("#{search}", #listening_link)
# replaces the song title (passed in as variable 'search') with the URL'ed title
end
I need to repeat this for many, many categories. I tried something like this to DRY the code but it didn't work:
def get_img_and_links(act, res, search, asin)
'#'+act+'ing_img' = res.items.first.get_hash('MediumImage')["URL"] # don't show an image
'#'+act+'ing_link' = create_amz_url(search, asin)
tempy = '#now'+act+'ing'.content.match(/#now"#{act}"ing.*/).to_s.gsub("#now#{act}ing",'') # strips away the #nowreading tag
'#now'+act+'ing'.content = tempy.match(/#{search}.*/).to_s.gsub("#{search}", '#'+act+'ing_link')
# replaces the book title (passed in as variable 'search') with the URL'ed title
end
Essentially, I was trying to create a function that took an "act" (e.g., "read", "listen", etc) and have the variables within that function be dynamic. Can this be accomplished? If so, how?
Look up instance_variable_set here: http://ruby-doc.org/core/classes/Object.html. It's what you need to dynamically create these variables.
instance_variable_set "##{act}ing_img".to_sym, res.items.first.get_hash('MediumImage')["URL"]
And so on...
Good looking out trying to dry up your code. I would definitely use some hashes there instead of instance variables. Then you can use the key as the action. Just a thought.
IMPO, I think you should use more generic variables. Although the creating variables are supported by ruby, it will make your code hard to read
my suggestion is having some generic names like
#img (instead of reading_img, listing_img etc..)
#link (instead of reading_link, listing_link etc..)
and something like #content, because as your login at a given time only one will be selected and this wouldn't be a problem
Again, this is what i understood from the code you posted and correct me if I'm wrong
cheers
sameera
you should do something like this:
def setup
#reading_img = get_img(whatever)
#reading_link = get_link(whatever)
#listening_img = get_img(whatever)
#listening_link = get_link(whatever)
end
def get_img(whatever)
...do stuff and return stuff...
end
def get_link(whatever)
...do stuff and return stuff...
end

How do I convert a string to a class method?

This is how to convert a string to a class in Rails/Ruby:
p = "Post"
Kernel.const_get(p)
eval(p)
p.constantize
But what if I am retrieving a method from an array/active record object like:
Post.description
but it could be
Post.anything
where anything is a string like anything = "description".
This is helpful since I want to refactor a very large class and reduce lines of code and repetition. How can I make it work?
Post.send(anything)
While eval can be a useful tool for this sort of thing, and those from other backgrounds may take to using it as often as one might a can opener, it's actually dangerous to use so casually. Eval implies that anything can happen if you're not careful.
A safer method is this:
on_class = "Post"
on_class.constantize.send("method_name")
on_class.constantize.send("method_name", arg1)
Object#send will call whatever method you want. You can send either a Symbol or a String and provided the method isn't private or protected, should work.
Since this is taged as a Ruby on Rails question, I'll elaborate just a little.
In Rails 3, assuming title is the name of a field on an ActiveRecord object, then the following is also valid:
#post = Post.new
method = "title"
#post.send(method) # => #post.title
#post.send("#{method}=","New Name") # => #post.title = "New Name"
Try this:
class Test
def method_missing(id, *args)
puts "#{id} - get your method name"
puts "#{args} - get values"
end
end
a = Test.new
a.name('123')
So the general syntax would be a.<anything>(<any argument>).

Resources